@vladimirven/openswe 0.1.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/AGENTS.md +203 -0
- package/CLAUDE.md +203 -0
- package/README.md +166 -0
- package/bun.lock +447 -0
- package/bunfig.toml +4 -0
- package/package.json +42 -0
- package/src/app.tsx +84 -0
- package/src/components/App.tsx +526 -0
- package/src/components/ConfirmDialog.tsx +88 -0
- package/src/components/Footer.tsx +50 -0
- package/src/components/HelpModal.tsx +136 -0
- package/src/components/IssueSelectorModal.tsx +701 -0
- package/src/components/ManualSessionModal.tsx +191 -0
- package/src/components/PhaseProgress.tsx +45 -0
- package/src/components/Preview.tsx +249 -0
- package/src/components/ProviderSwitcherModal.tsx +156 -0
- package/src/components/ScrollableText.tsx +120 -0
- package/src/components/SessionCard.tsx +60 -0
- package/src/components/SessionList.tsx +79 -0
- package/src/components/SessionTerminal.tsx +89 -0
- package/src/components/StatusBar.tsx +84 -0
- package/src/components/ThemeSwitcherModal.tsx +237 -0
- package/src/components/index.ts +58 -0
- package/src/components/session-utils.ts +337 -0
- package/src/components/theme.ts +206 -0
- package/src/components/types.ts +215 -0
- package/src/config/defaults.ts +44 -0
- package/src/config/env.ts +67 -0
- package/src/config/global.ts +252 -0
- package/src/config/index.ts +171 -0
- package/src/config/types.ts +131 -0
- package/src/core/.gitkeep +0 -0
- package/src/core/index.ts +5 -0
- package/src/core/parser.ts +62 -0
- package/src/core/process-manager.ts +52 -0
- package/src/core/session.ts +423 -0
- package/src/core/tmux.ts +206 -0
- package/src/git/.gitkeep +0 -0
- package/src/git/index.ts +8 -0
- package/src/git/repo.ts +443 -0
- package/src/git/worktree.ts +317 -0
- package/src/github/.gitkeep +0 -0
- package/src/github/client.ts +208 -0
- package/src/github/index.ts +8 -0
- package/src/github/issues.ts +351 -0
- package/src/index.ts +369 -0
- package/src/prompts/.gitkeep +0 -0
- package/src/prompts/index.ts +1 -0
- package/src/prompts/swe-system.ts +22 -0
- package/src/providers/claude.ts +103 -0
- package/src/providers/index.ts +21 -0
- package/src/providers/opencode.ts +98 -0
- package/src/providers/registry.ts +53 -0
- package/src/providers/types.ts +117 -0
- package/src/store/buffers.ts +234 -0
- package/src/store/db.test.ts +579 -0
- package/src/store/db.ts +249 -0
- package/src/store/index.ts +101 -0
- package/src/store/project.ts +119 -0
- package/src/store/schema.sql +71 -0
- package/src/store/sessions.ts +454 -0
- package/src/store/types.ts +194 -0
- package/src/theme/context.tsx +170 -0
- package/src/theme/custom.ts +134 -0
- package/src/theme/index.ts +58 -0
- package/src/theme/loader.ts +264 -0
- package/src/theme/themes/aura.json +69 -0
- package/src/theme/themes/ayu.json +80 -0
- package/src/theme/themes/carbonfox.json +248 -0
- package/src/theme/themes/catppuccin-frappe.json +233 -0
- package/src/theme/themes/catppuccin-macchiato.json +233 -0
- package/src/theme/themes/catppuccin.json +112 -0
- package/src/theme/themes/cobalt2.json +228 -0
- package/src/theme/themes/cursor.json +249 -0
- package/src/theme/themes/dracula.json +219 -0
- package/src/theme/themes/everforest.json +241 -0
- package/src/theme/themes/flexoki.json +237 -0
- package/src/theme/themes/github.json +233 -0
- package/src/theme/themes/gruvbox.json +242 -0
- package/src/theme/themes/kanagawa.json +77 -0
- package/src/theme/themes/lucent-orng.json +237 -0
- package/src/theme/themes/material.json +235 -0
- package/src/theme/themes/matrix.json +77 -0
- package/src/theme/themes/mercury.json +252 -0
- package/src/theme/themes/monokai.json +221 -0
- package/src/theme/themes/nightowl.json +221 -0
- package/src/theme/themes/nord.json +223 -0
- package/src/theme/themes/one-dark.json +84 -0
- package/src/theme/themes/opencode.json +245 -0
- package/src/theme/themes/orng.json +249 -0
- package/src/theme/themes/osaka-jade.json +93 -0
- package/src/theme/themes/palenight.json +222 -0
- package/src/theme/themes/rosepine.json +234 -0
- package/src/theme/themes/solarized.json +223 -0
- package/src/theme/themes/synthwave84.json +226 -0
- package/src/theme/themes/tokyonight.json +243 -0
- package/src/theme/themes/vercel.json +245 -0
- package/src/theme/themes/vesper.json +218 -0
- package/src/theme/themes/zenburn.json +223 -0
- package/src/theme/types.ts +225 -0
- package/src/types/sql.d.ts +4 -0
- package/src/utils/ansi-parser.ts +225 -0
- package/src/utils/format.ts +46 -0
- package/src/utils/id.ts +15 -0
- package/src/utils/logger.ts +112 -0
- package/src/utils/prerequisites.ts +118 -0
- package/src/utils/shell.ts +9 -0
- package/src/wizard/flows.ts +419 -0
- package/src/wizard/index.ts +37 -0
- package/src/wizard/prompts.ts +190 -0
- package/src/workspace/detect.test.ts +51 -0
- package/src/workspace/detect.ts +223 -0
- package/src/workspace/index.ts +71 -0
- package/src/workspace/init.ts +131 -0
- package/src/workspace/paths.ts +143 -0
- package/src/workspace/project.ts +164 -0
- package/tsconfig.json +22 -0
|
@@ -0,0 +1,701 @@
|
|
|
1
|
+
import { createSignal, onMount, For, Show, createEffect, onCleanup } from "solid-js"
|
|
2
|
+
import { useKeyboard } from "@opentui/solid"
|
|
3
|
+
import type { IssueSelectorModalProps } from "./types"
|
|
4
|
+
import type { Session } from "../store"
|
|
5
|
+
import { fetchIssues, formatRelativeTime, type GitHubIssue, type IssueState } from "../github"
|
|
6
|
+
import { createSessionFromIssue, findNextAvailableWorktreeName } from "./session-utils"
|
|
7
|
+
import { ScrollableText } from "./ScrollableText"
|
|
8
|
+
import { useColors, borders } from "./theme"
|
|
9
|
+
import { Footer } from "./Footer"
|
|
10
|
+
import { logger } from "../utils/logger"
|
|
11
|
+
|
|
12
|
+
// Bold attribute constant
|
|
13
|
+
const BOLD = 1
|
|
14
|
+
const ITEMS_PER_PAGE = 7 // Increased slightly since rows are more compact
|
|
15
|
+
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
|
16
|
+
|
|
17
|
+
/** State filter options */
|
|
18
|
+
const STATE_FILTERS: IssueState[] = ["open", "closed", "all"]
|
|
19
|
+
|
|
20
|
+
export function IssueSelectorModal(props: IssueSelectorModalProps) {
|
|
21
|
+
const colors = useColors()
|
|
22
|
+
|
|
23
|
+
// ============================================================================
|
|
24
|
+
// State
|
|
25
|
+
// ============================================================================
|
|
26
|
+
|
|
27
|
+
const [issues, setIssues] = createSignal<GitHubIssue[]>([])
|
|
28
|
+
const [selectedIndices, setSelectedIndices] = createSignal<Set<number>>(new Set())
|
|
29
|
+
const [focusedIndex, setFocusedIndex] = createSignal(0)
|
|
30
|
+
const [scrollOffset, setScrollOffset] = createSignal(0)
|
|
31
|
+
const [stateFilter, setStateFilter] = createSignal<IssueState>("open")
|
|
32
|
+
const [loading, setLoading] = createSignal(true)
|
|
33
|
+
const [error, setError] = createSignal<string | null>(null)
|
|
34
|
+
const [creating, setCreating] = createSignal(false)
|
|
35
|
+
const [spinnerFrame, setSpinnerFrame] = createSignal(0)
|
|
36
|
+
const [resolving, setResolving] = createSignal(false)
|
|
37
|
+
|
|
38
|
+
// Animation effect
|
|
39
|
+
createEffect(() => {
|
|
40
|
+
if (creating()) {
|
|
41
|
+
const interval = setInterval(() => {
|
|
42
|
+
setSpinnerFrame((f) => (f + 1) % SPINNER_FRAMES.length)
|
|
43
|
+
}, 80)
|
|
44
|
+
onCleanup(() => clearInterval(interval))
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
// Conflict resolution state
|
|
49
|
+
const [conflictIssue, setConflictIssue] = createSignal<GitHubIssue | null>(null)
|
|
50
|
+
const [suggestedName, setSuggestedName] = createSignal<string>("")
|
|
51
|
+
const [conflictChoice, setConflictChoice] = createSignal<0 | 1 | 2>(0) // 0: Use existing, 1: Create new, 2: Overwrite
|
|
52
|
+
const [pendingIndices, setPendingIndices] = createSignal<number[]>([])
|
|
53
|
+
const [successCount, setSuccessCount] = createSignal(0)
|
|
54
|
+
const [createdSessions, setCreatedSessions] = createSignal<Session[]>([])
|
|
55
|
+
|
|
56
|
+
// ============================================================================
|
|
57
|
+
// Data Loading
|
|
58
|
+
// ============================================================================
|
|
59
|
+
|
|
60
|
+
const loadIssues = async () => {
|
|
61
|
+
setLoading(true)
|
|
62
|
+
setError(null)
|
|
63
|
+
|
|
64
|
+
logger.debug("Loading issues", {
|
|
65
|
+
repo: props.ownerRepo,
|
|
66
|
+
state: stateFilter(),
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
const result = await fetchIssues(props.ownerRepo, {
|
|
70
|
+
state: stateFilter(),
|
|
71
|
+
limit: 500,
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
setLoading(false)
|
|
75
|
+
|
|
76
|
+
if (result.success) {
|
|
77
|
+
setIssues(result.issues)
|
|
78
|
+
setFocusedIndex(0)
|
|
79
|
+
setScrollOffset(0)
|
|
80
|
+
setSelectedIndices(new Set<number>())
|
|
81
|
+
logger.debug("Issues loaded", { count: result.issues.length })
|
|
82
|
+
} else {
|
|
83
|
+
setError(result.error ?? "Failed to fetch issues")
|
|
84
|
+
setIssues([])
|
|
85
|
+
logger.warn("Issue fetch failed", result.error)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
onMount(() => {
|
|
90
|
+
loadIssues()
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
// ============================================================================
|
|
94
|
+
// Actions
|
|
95
|
+
// ============================================================================
|
|
96
|
+
|
|
97
|
+
const toggleSelection = (index: number) => {
|
|
98
|
+
setSelectedIndices((prev) => {
|
|
99
|
+
const newSet = new Set<number>(prev)
|
|
100
|
+
if (newSet.has(index)) {
|
|
101
|
+
newSet.delete(index)
|
|
102
|
+
} else {
|
|
103
|
+
newSet.add(index)
|
|
104
|
+
}
|
|
105
|
+
return newSet
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const cycleStateFilter = () => {
|
|
110
|
+
const currentIndex = STATE_FILTERS.indexOf(stateFilter())
|
|
111
|
+
const nextIndex = (currentIndex + 1) % STATE_FILTERS.length
|
|
112
|
+
setStateFilter(STATE_FILTERS[nextIndex]!)
|
|
113
|
+
loadIssues()
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Recursive function to process the queue of selected issues
|
|
117
|
+
const processNextIssue = async () => {
|
|
118
|
+
// Check if cancelled
|
|
119
|
+
if (!creating()) return
|
|
120
|
+
|
|
121
|
+
const indices = pendingIndices()
|
|
122
|
+
if (indices.length === 0) {
|
|
123
|
+
// All done
|
|
124
|
+
finishCreation()
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const index = indices[0]! // Get first
|
|
129
|
+
const issueList = issues()
|
|
130
|
+
const issue = issueList[index]
|
|
131
|
+
|
|
132
|
+
if (!issue) {
|
|
133
|
+
// Skip invalid index
|
|
134
|
+
setPendingIndices(indices.slice(1))
|
|
135
|
+
await processNextIssue()
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Try to create session normally first
|
|
140
|
+
const result = await createSessionFromIssue(props.projectRoot, issue, {
|
|
141
|
+
aiSessionData: { backend: props.currentBackend }
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
// Check if cancelled during await
|
|
145
|
+
if (!creating()) return
|
|
146
|
+
|
|
147
|
+
if (result.success && result.session) {
|
|
148
|
+
setSuccessCount((c) => c + 1)
|
|
149
|
+
setCreatedSessions((prev) => [...prev, result.session!])
|
|
150
|
+
logger.debug("Session created from issue", { issueNumber: issue.number })
|
|
151
|
+
// Remove from queue and continue
|
|
152
|
+
setPendingIndices(indices.slice(1))
|
|
153
|
+
await processNextIssue()
|
|
154
|
+
} else if (!result.success) {
|
|
155
|
+
// Check if it's a conflict
|
|
156
|
+
if (result.error?.toLowerCase().includes("already exists")) {
|
|
157
|
+
// Find suggested name
|
|
158
|
+
const suggestion = await findNextAvailableWorktreeName(props.projectRoot, issue.number)
|
|
159
|
+
|
|
160
|
+
// Check if cancelled during await
|
|
161
|
+
if (!creating()) return
|
|
162
|
+
|
|
163
|
+
// Pause and show conflict UI
|
|
164
|
+
setConflictIssue(issue)
|
|
165
|
+
setSuggestedName(suggestion)
|
|
166
|
+
setConflictChoice(0) // Default to "Continue"
|
|
167
|
+
// Don't remove from queue yet, we'll process it after resolution
|
|
168
|
+
} else {
|
|
169
|
+
// Other error, just log and continue
|
|
170
|
+
logger.warn("Failed to create session from issue", {
|
|
171
|
+
issueNumber: issue.number,
|
|
172
|
+
error: result.error,
|
|
173
|
+
})
|
|
174
|
+
setError(result.error ?? "Failed to create session")
|
|
175
|
+
|
|
176
|
+
// Remove from queue and continue
|
|
177
|
+
setPendingIndices(indices.slice(1))
|
|
178
|
+
await processNextIssue()
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const resolveConflict = async () => {
|
|
184
|
+
if (resolving()) return
|
|
185
|
+
const issue = conflictIssue()
|
|
186
|
+
if (!issue) return
|
|
187
|
+
|
|
188
|
+
setResolving(true)
|
|
189
|
+
const choice = conflictChoice()
|
|
190
|
+
const suggestion = suggestedName()
|
|
191
|
+
|
|
192
|
+
// Create with chosen strategy
|
|
193
|
+
const result = await createSessionFromIssue(props.projectRoot, issue, {
|
|
194
|
+
worktreeNameOverride: choice === 1 ? suggestion : undefined,
|
|
195
|
+
overwriteWorktreeChoice: choice, // Maps directly to OverwriteWorktreeChoice enum
|
|
196
|
+
aiSessionData: { backend: props.currentBackend }
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
setResolving(false)
|
|
200
|
+
|
|
201
|
+
// Check if cancelled during await
|
|
202
|
+
if (!creating()) return
|
|
203
|
+
|
|
204
|
+
if (result.success && result.session) {
|
|
205
|
+
setSuccessCount((c) => c + 1)
|
|
206
|
+
setCreatedSessions((prev) => [...prev, result.session!])
|
|
207
|
+
logger.debug("Session created after conflict resolution", { issueNumber: issue.number })
|
|
208
|
+
} else {
|
|
209
|
+
logger.warn("Failed to create session after conflict resolution", {
|
|
210
|
+
issueNumber: issue.number,
|
|
211
|
+
error: result.error,
|
|
212
|
+
})
|
|
213
|
+
setError(result.error ?? "Failed to create session")
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Clear conflict state
|
|
217
|
+
setConflictIssue(null)
|
|
218
|
+
setSuggestedName("")
|
|
219
|
+
|
|
220
|
+
// Remove current issue from queue (it's done now)
|
|
221
|
+
setPendingIndices((prev) => prev.slice(1))
|
|
222
|
+
|
|
223
|
+
// Continue processing
|
|
224
|
+
await processNextIssue()
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const finishCreation = () => {
|
|
228
|
+
setCreating(false)
|
|
229
|
+
const created = createdSessions()
|
|
230
|
+
if (created.length > 0) {
|
|
231
|
+
props.onSessionsCreated(created)
|
|
232
|
+
props.onClose()
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const startCreation = async () => {
|
|
237
|
+
let selected = new Set(selectedIndices())
|
|
238
|
+
|
|
239
|
+
// Auto-select focused if nothing selected
|
|
240
|
+
if (selected.size === 0 && issues().length > 0) {
|
|
241
|
+
const focused = focusedIndex()
|
|
242
|
+
selected.add(focused)
|
|
243
|
+
toggleSelection(focused)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (selected.size === 0) return
|
|
247
|
+
|
|
248
|
+
setCreating(true)
|
|
249
|
+
setSuccessCount(0)
|
|
250
|
+
setError(null)
|
|
251
|
+
setCreatedSessions([])
|
|
252
|
+
|
|
253
|
+
// Initialize queue
|
|
254
|
+
setPendingIndices(Array.from(selected))
|
|
255
|
+
|
|
256
|
+
logger.debug("Creating sessions from selected issues", {
|
|
257
|
+
selectedCount: selected.size,
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
// Start processing
|
|
261
|
+
await processNextIssue()
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ============================================================================
|
|
265
|
+
// Keyboard Handling
|
|
266
|
+
// ============================================================================
|
|
267
|
+
|
|
268
|
+
useKeyboard((event) => {
|
|
269
|
+
// If conflict UI is open, handle keys there
|
|
270
|
+
if (conflictIssue()) {
|
|
271
|
+
switch (event.name) {
|
|
272
|
+
case "up":
|
|
273
|
+
case "k":
|
|
274
|
+
if (conflictChoice() != 0) {
|
|
275
|
+
setConflictChoice((conflictChoice() - 1) as 0 | 1 | 2)
|
|
276
|
+
}
|
|
277
|
+
break
|
|
278
|
+
case "down":
|
|
279
|
+
case "j":
|
|
280
|
+
if (conflictChoice() != 2) {
|
|
281
|
+
setConflictChoice((conflictChoice() + 1) as 0 | 1 | 2)
|
|
282
|
+
}
|
|
283
|
+
break
|
|
284
|
+
case "enter":
|
|
285
|
+
case "return":
|
|
286
|
+
resolveConflict()
|
|
287
|
+
break
|
|
288
|
+
case "escape":
|
|
289
|
+
// Cancel everything
|
|
290
|
+
setConflictIssue(null)
|
|
291
|
+
setPendingIndices([])
|
|
292
|
+
setCreating(false)
|
|
293
|
+
break
|
|
294
|
+
}
|
|
295
|
+
return
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const issueList = issues()
|
|
299
|
+
|
|
300
|
+
// Allow Escape to cancel creation or close modal
|
|
301
|
+
if (event.name === "escape") {
|
|
302
|
+
if (creating()) {
|
|
303
|
+
// Cancel creation
|
|
304
|
+
setPendingIndices([])
|
|
305
|
+
setCreating(false)
|
|
306
|
+
setConflictIssue(null)
|
|
307
|
+
setResolving(false)
|
|
308
|
+
} else {
|
|
309
|
+
props.onClose()
|
|
310
|
+
}
|
|
311
|
+
return
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (loading()) return
|
|
315
|
+
|
|
316
|
+
// If creating, only handle specific keys for conflict
|
|
317
|
+
if (creating()) {
|
|
318
|
+
if (conflictIssue()) {
|
|
319
|
+
switch (event.name) {
|
|
320
|
+
case "up":
|
|
321
|
+
case "k":
|
|
322
|
+
if (conflictChoice() != 0) {
|
|
323
|
+
setConflictChoice((conflictChoice() - 1) as 0 | 1 | 2)
|
|
324
|
+
}
|
|
325
|
+
break
|
|
326
|
+
case "down":
|
|
327
|
+
case "j":
|
|
328
|
+
if (conflictChoice() != 2) {
|
|
329
|
+
setConflictChoice((conflictChoice() + 1) as 0 | 1 | 2)
|
|
330
|
+
}
|
|
331
|
+
break
|
|
332
|
+
case "enter":
|
|
333
|
+
case "return":
|
|
334
|
+
resolveConflict()
|
|
335
|
+
break
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
switch (event.name) {
|
|
342
|
+
case "j":
|
|
343
|
+
case "down":
|
|
344
|
+
if (issueList.length > 0) {
|
|
345
|
+
const nextIndex = Math.min(focusedIndex() + 1, issueList.length - 1)
|
|
346
|
+
setFocusedIndex(nextIndex)
|
|
347
|
+
if (nextIndex >= scrollOffset() + ITEMS_PER_PAGE) {
|
|
348
|
+
setScrollOffset(nextIndex - ITEMS_PER_PAGE + 1)
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
break
|
|
352
|
+
|
|
353
|
+
case "k":
|
|
354
|
+
case "up":
|
|
355
|
+
if (issueList.length > 0) {
|
|
356
|
+
const prevIndex = Math.max(focusedIndex() - 1, 0)
|
|
357
|
+
setFocusedIndex(prevIndex)
|
|
358
|
+
if (prevIndex < scrollOffset()) {
|
|
359
|
+
setScrollOffset(prevIndex)
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
break
|
|
363
|
+
|
|
364
|
+
case "space":
|
|
365
|
+
if (issueList.length > 0) {
|
|
366
|
+
toggleSelection(focusedIndex())
|
|
367
|
+
}
|
|
368
|
+
break
|
|
369
|
+
|
|
370
|
+
case "tab":
|
|
371
|
+
cycleStateFilter()
|
|
372
|
+
break
|
|
373
|
+
|
|
374
|
+
case "return":
|
|
375
|
+
startCreation()
|
|
376
|
+
break
|
|
377
|
+
|
|
378
|
+
case "a":
|
|
379
|
+
// Select all
|
|
380
|
+
if (issueList.length > 0) {
|
|
381
|
+
const allIndices = new Set<number>(issueList.map((_, i) => i))
|
|
382
|
+
if (selectedIndices().size === issueList.length) {
|
|
383
|
+
setSelectedIndices(new Set<number>())
|
|
384
|
+
} else {
|
|
385
|
+
setSelectedIndices(allIndices)
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
break
|
|
389
|
+
|
|
390
|
+
case "r":
|
|
391
|
+
// Refresh
|
|
392
|
+
loadIssues()
|
|
393
|
+
break
|
|
394
|
+
}
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
// ============================================================================
|
|
398
|
+
// Render Helpers
|
|
399
|
+
// ============================================================================
|
|
400
|
+
|
|
401
|
+
const truncate = (text: string, maxLen: number): string => {
|
|
402
|
+
if (text.length <= maxLen) return text
|
|
403
|
+
return text.slice(0, maxLen - 1) + "…"
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const formatLabels = (labels: { name: string }[]): string => {
|
|
407
|
+
if (labels.length === 0) return ""
|
|
408
|
+
const names = labels.slice(0, 2).map((l) => `[${l.name}]`)
|
|
409
|
+
if (labels.length > 2) {
|
|
410
|
+
names.push(`+${labels.length - 2}`)
|
|
411
|
+
}
|
|
412
|
+
return names.join(" ")
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// ============================================================================
|
|
416
|
+
// Render
|
|
417
|
+
// ============================================================================
|
|
418
|
+
|
|
419
|
+
const modalWidth = 80
|
|
420
|
+
const modalHeight = 24
|
|
421
|
+
const selectedCount = () => selectedIndices().size
|
|
422
|
+
|
|
423
|
+
return (
|
|
424
|
+
<box
|
|
425
|
+
position="absolute"
|
|
426
|
+
top={0}
|
|
427
|
+
left={0}
|
|
428
|
+
width="100%"
|
|
429
|
+
height="100%"
|
|
430
|
+
justifyContent="center"
|
|
431
|
+
alignItems="center"
|
|
432
|
+
>
|
|
433
|
+
{/* Modal container */}
|
|
434
|
+
<box
|
|
435
|
+
flexDirection="column"
|
|
436
|
+
width={modalWidth}
|
|
437
|
+
height={modalHeight}
|
|
438
|
+
backgroundColor={colors().bg.secondary}
|
|
439
|
+
borderStyle="rounded"
|
|
440
|
+
borderColor={colors().border.accent}
|
|
441
|
+
overflow="hidden"
|
|
442
|
+
>
|
|
443
|
+
<Show when={creating()} fallback={
|
|
444
|
+
// Standard Issue List View
|
|
445
|
+
<>
|
|
446
|
+
{/* Header with Tabs */}
|
|
447
|
+
<box
|
|
448
|
+
height={1}
|
|
449
|
+
paddingLeft={1}
|
|
450
|
+
paddingRight={1}
|
|
451
|
+
justifyContent="space-between"
|
|
452
|
+
backgroundColor={colors().bg.secondary}
|
|
453
|
+
>
|
|
454
|
+
<box flexDirection="row" flexGrow={1} overflow="hidden" marginRight={2}>
|
|
455
|
+
<text fg={colors().text.primary} attributes={BOLD}>
|
|
456
|
+
{props.ownerRepo}
|
|
457
|
+
</text>
|
|
458
|
+
</box>
|
|
459
|
+
|
|
460
|
+
{/* Filter Tabs */}
|
|
461
|
+
<box flexDirection="row" gap={1}>
|
|
462
|
+
<For each={STATE_FILTERS}>
|
|
463
|
+
{(state) => (
|
|
464
|
+
<text
|
|
465
|
+
fg={stateFilter() === state ? colors().accent.primary : colors().text.muted}
|
|
466
|
+
attributes={stateFilter() === state ? BOLD : 0}
|
|
467
|
+
>
|
|
468
|
+
{stateFilter() === state ? `[ ${state} ]` : ` ${state} `}
|
|
469
|
+
</text>
|
|
470
|
+
)}
|
|
471
|
+
</For>
|
|
472
|
+
</box>
|
|
473
|
+
</box>
|
|
474
|
+
|
|
475
|
+
{/* Content area */}
|
|
476
|
+
<box
|
|
477
|
+
flexDirection="column"
|
|
478
|
+
flexGrow={1}
|
|
479
|
+
paddingTop={1}
|
|
480
|
+
overflow="hidden"
|
|
481
|
+
>
|
|
482
|
+
<Show when={loading()}>
|
|
483
|
+
<box justifyContent="center" alignItems="center" flexGrow={1}>
|
|
484
|
+
<text fg={colors().text.muted}>Loading issues...</text>
|
|
485
|
+
</box>
|
|
486
|
+
</Show>
|
|
487
|
+
|
|
488
|
+
<Show when={error() && !loading()}>
|
|
489
|
+
<box justifyContent="center" alignItems="center" flexGrow={1}>
|
|
490
|
+
<text fg={colors().accent.error}>{error()}</text>
|
|
491
|
+
</box>
|
|
492
|
+
</Show>
|
|
493
|
+
|
|
494
|
+
<Show when={!loading() && !error() && issues().length === 0}>
|
|
495
|
+
<box justifyContent="center" alignItems="center" flexGrow={1}>
|
|
496
|
+
<text fg={colors().text.muted}>No issues found</text>
|
|
497
|
+
</box>
|
|
498
|
+
</Show>
|
|
499
|
+
|
|
500
|
+
<Show when={!loading() && !error() && issues().length > 0}>
|
|
501
|
+
<box
|
|
502
|
+
flexDirection="column"
|
|
503
|
+
flexGrow={1}
|
|
504
|
+
width="100%"
|
|
505
|
+
>
|
|
506
|
+
<For each={issues().slice(scrollOffset(), scrollOffset() + ITEMS_PER_PAGE)}>
|
|
507
|
+
{(issue, i) => {
|
|
508
|
+
const index = () => scrollOffset() + i()
|
|
509
|
+
const isFocused = () => focusedIndex() === index()
|
|
510
|
+
const isSelected = () => selectedIndices().has(index())
|
|
511
|
+
|
|
512
|
+
const textColor = () => isFocused() ? colors().text.primary : colors().text.secondary
|
|
513
|
+
const mutedColor = () => isFocused() ? colors().text.primary : colors().text.muted
|
|
514
|
+
|
|
515
|
+
return (
|
|
516
|
+
<box
|
|
517
|
+
flexDirection="row"
|
|
518
|
+
height={1} // Single line rows
|
|
519
|
+
backgroundColor={isFocused() ? colors().bg.cardSelected : undefined}
|
|
520
|
+
paddingLeft={0}
|
|
521
|
+
paddingRight={1}
|
|
522
|
+
alignItems="center"
|
|
523
|
+
overflow="hidden"
|
|
524
|
+
>
|
|
525
|
+
{/* Focus Indicator */}
|
|
526
|
+
<box width={2}>
|
|
527
|
+
<text fg={colors().accent.primary}>
|
|
528
|
+
{isFocused() ? "▍" : " "}
|
|
529
|
+
</text>
|
|
530
|
+
</box>
|
|
531
|
+
|
|
532
|
+
{/* Selection Checkbox */}
|
|
533
|
+
<box width={4}>
|
|
534
|
+
<text
|
|
535
|
+
fg={isSelected()
|
|
536
|
+
? (isFocused() ? colors().text.primary : colors().accent.success)
|
|
537
|
+
: mutedColor()
|
|
538
|
+
}
|
|
539
|
+
attributes={isSelected() ? BOLD : 0}
|
|
540
|
+
>
|
|
541
|
+
{isSelected() ? "[✓]" : "[ ]"}
|
|
542
|
+
</text>
|
|
543
|
+
</box>
|
|
544
|
+
|
|
545
|
+
{/* Issue ID (Fixed width, right alignedish) */}
|
|
546
|
+
<box width={6}>
|
|
547
|
+
<text fg={textColor()}>
|
|
548
|
+
{`#${issue.number}`.padEnd(5)}
|
|
549
|
+
</text>
|
|
550
|
+
</box>
|
|
551
|
+
|
|
552
|
+
{/* Title (Flex) */}
|
|
553
|
+
<box flexGrow={1} marginRight={2} overflow="hidden">
|
|
554
|
+
<ScrollableText
|
|
555
|
+
text={issue.title}
|
|
556
|
+
width={30}
|
|
557
|
+
isActive={isFocused()}
|
|
558
|
+
fg={isFocused() ? colors().text.primary : colors().text.secondary}
|
|
559
|
+
attributes={isFocused() ? BOLD : 0}
|
|
560
|
+
/>
|
|
561
|
+
</box>
|
|
562
|
+
|
|
563
|
+
{/* Labels (Variable) */}
|
|
564
|
+
<box width={20} overflow="hidden">
|
|
565
|
+
<Show when={issue.labels.length > 0}>
|
|
566
|
+
<text fg={mutedColor()}>
|
|
567
|
+
{formatLabels(issue.labels)}
|
|
568
|
+
</text>
|
|
569
|
+
</Show>
|
|
570
|
+
</box>
|
|
571
|
+
|
|
572
|
+
{/* Time (Right aligned) */}
|
|
573
|
+
<box width={12} justifyContent="flex-end">
|
|
574
|
+
<text fg={mutedColor()}>
|
|
575
|
+
{formatRelativeTime(issue.updatedAt)}
|
|
576
|
+
</text>
|
|
577
|
+
</box>
|
|
578
|
+
</box>
|
|
579
|
+
)
|
|
580
|
+
}}
|
|
581
|
+
</For>
|
|
582
|
+
</box>
|
|
583
|
+
</Show>
|
|
584
|
+
</box>
|
|
585
|
+
|
|
586
|
+
{/* Footer */}
|
|
587
|
+
<Footer
|
|
588
|
+
bgColor={colors().bg.primary}
|
|
589
|
+
actions={[
|
|
590
|
+
{ key: "Space", label: "Toggle" },
|
|
591
|
+
{ key: "a", label: "All" },
|
|
592
|
+
{ key: "Tab", label: "Filter" },
|
|
593
|
+
...(selectedCount() > 0
|
|
594
|
+
? [{ key: "Enter", label: `Create (${selectedCount()})` }]
|
|
595
|
+
: [{ key: "Enter", label: "Create" }]
|
|
596
|
+
),
|
|
597
|
+
{ key: "Esc", label: "Close" },
|
|
598
|
+
]}
|
|
599
|
+
/>
|
|
600
|
+
</>
|
|
601
|
+
}>
|
|
602
|
+
{/* Creating / Conflict State */}
|
|
603
|
+
<Show when={!!conflictIssue()} fallback={
|
|
604
|
+
// Progress View
|
|
605
|
+
<box flexDirection="column" width="100%" height="100%" justifyContent="center" alignItems="center">
|
|
606
|
+
<text fg={colors().accent.primary} attributes={BOLD}>Creating Sessions...</text>
|
|
607
|
+
<box height={1} />
|
|
608
|
+
<box flexDirection="row" gap={1}>
|
|
609
|
+
<text fg={colors().accent.primary}>{SPINNER_FRAMES[spinnerFrame()]}</text>
|
|
610
|
+
<text fg={colors().text.secondary}>
|
|
611
|
+
Processing {successCount() + 1} of {selectedCount() || 1}
|
|
612
|
+
</text>
|
|
613
|
+
</box>
|
|
614
|
+
|
|
615
|
+
{/* Current Issue being processed */}
|
|
616
|
+
<Show when={pendingIndices().length > 0}>
|
|
617
|
+
<box marginTop={1} padding={1} borderColor={colors().border.secondary} borderStyle="rounded">
|
|
618
|
+
<text fg={colors().text.primary}>
|
|
619
|
+
#{issues()[pendingIndices()[0]!]?.number} {truncate(issues()[pendingIndices()[0]!]?.title || "", 40)}
|
|
620
|
+
</text>
|
|
621
|
+
</box>
|
|
622
|
+
</Show>
|
|
623
|
+
|
|
624
|
+
<box height={2} />
|
|
625
|
+
<text fg={colors().text.muted}>Please wait...</text>
|
|
626
|
+
</box>
|
|
627
|
+
}>
|
|
628
|
+
{/* Conflict Resolution View */}
|
|
629
|
+
<box flexDirection="column" width="100%" height="100%">
|
|
630
|
+
<box flexDirection="column" flexGrow={1} padding={2} justifyContent="center" alignItems="center">
|
|
631
|
+
<text fg={colors().accent.warning} attributes={BOLD}>Worktree Conflict</text>
|
|
632
|
+
|
|
633
|
+
<box height={1} />
|
|
634
|
+
|
|
635
|
+
<text fg={colors().text.primary}>
|
|
636
|
+
Worktree for issue #{conflictIssue()?.number} already exists.
|
|
637
|
+
</text>
|
|
638
|
+
<text fg={colors().text.secondary}>
|
|
639
|
+
Please select an action:
|
|
640
|
+
</text>
|
|
641
|
+
|
|
642
|
+
<box height={2} />
|
|
643
|
+
|
|
644
|
+
{/* Options */}
|
|
645
|
+
<box flexDirection="column" gap={1} width={50}>
|
|
646
|
+
{/* Continue worktree option*/}
|
|
647
|
+
<box flexDirection="row" gap={1}>
|
|
648
|
+
<text fg={conflictChoice() === 0 ? colors().accent.primary : colors().text.muted}>
|
|
649
|
+
{conflictChoice() === 0 ? "●" : "○"}
|
|
650
|
+
</text>
|
|
651
|
+
<text
|
|
652
|
+
fg={conflictChoice() === 0 ? colors().text.primary : colors().text.secondary}
|
|
653
|
+
attributes={conflictChoice() === 0 ? BOLD : 0}
|
|
654
|
+
>
|
|
655
|
+
Continue with existing worktree
|
|
656
|
+
</text>
|
|
657
|
+
</box>
|
|
658
|
+
|
|
659
|
+
{/* Create new option */}
|
|
660
|
+
<box flexDirection="row" gap={1}>
|
|
661
|
+
<text fg={conflictChoice() === 1 ? colors().accent.primary : colors().text.muted}>
|
|
662
|
+
{conflictChoice() === 1 ? "●" : "○"}
|
|
663
|
+
</text>
|
|
664
|
+
<text
|
|
665
|
+
fg={conflictChoice() === 1 ? colors().text.primary : colors().text.secondary}
|
|
666
|
+
attributes={conflictChoice() === 1 ? BOLD : 0}
|
|
667
|
+
>
|
|
668
|
+
Create new worktree ({suggestedName()})
|
|
669
|
+
</text>
|
|
670
|
+
</box>
|
|
671
|
+
|
|
672
|
+
{/* Overwrite option */}
|
|
673
|
+
<box flexDirection="row" gap={1}>
|
|
674
|
+
<text fg={conflictChoice() === 2 ? colors().accent.primary : colors().text.muted}>
|
|
675
|
+
{conflictChoice() === 2 ? "●" : "○"}
|
|
676
|
+
</text>
|
|
677
|
+
<text
|
|
678
|
+
fg={conflictChoice() === 2 ? colors().text.primary : colors().text.secondary}
|
|
679
|
+
attributes={conflictChoice() === 2 ? BOLD : 0}
|
|
680
|
+
>
|
|
681
|
+
Overwrite worktree
|
|
682
|
+
</text>
|
|
683
|
+
</box>
|
|
684
|
+
</box>
|
|
685
|
+
</box>
|
|
686
|
+
|
|
687
|
+
<Footer
|
|
688
|
+
bgColor={colors().bg.primary}
|
|
689
|
+
actions={[
|
|
690
|
+
{ key: "Enter", label: resolving() ? "Resolving..." : "Confirm" },
|
|
691
|
+
{ key: "Esc", label: "Cancel Batch" },
|
|
692
|
+
]}
|
|
693
|
+
/>
|
|
694
|
+
</box>
|
|
695
|
+
</Show>
|
|
696
|
+
</Show>
|
|
697
|
+
</box>
|
|
698
|
+
|
|
699
|
+
</box>
|
|
700
|
+
)
|
|
701
|
+
}
|