@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.
Files changed (117) hide show
  1. package/AGENTS.md +203 -0
  2. package/CLAUDE.md +203 -0
  3. package/README.md +166 -0
  4. package/bun.lock +447 -0
  5. package/bunfig.toml +4 -0
  6. package/package.json +42 -0
  7. package/src/app.tsx +84 -0
  8. package/src/components/App.tsx +526 -0
  9. package/src/components/ConfirmDialog.tsx +88 -0
  10. package/src/components/Footer.tsx +50 -0
  11. package/src/components/HelpModal.tsx +136 -0
  12. package/src/components/IssueSelectorModal.tsx +701 -0
  13. package/src/components/ManualSessionModal.tsx +191 -0
  14. package/src/components/PhaseProgress.tsx +45 -0
  15. package/src/components/Preview.tsx +249 -0
  16. package/src/components/ProviderSwitcherModal.tsx +156 -0
  17. package/src/components/ScrollableText.tsx +120 -0
  18. package/src/components/SessionCard.tsx +60 -0
  19. package/src/components/SessionList.tsx +79 -0
  20. package/src/components/SessionTerminal.tsx +89 -0
  21. package/src/components/StatusBar.tsx +84 -0
  22. package/src/components/ThemeSwitcherModal.tsx +237 -0
  23. package/src/components/index.ts +58 -0
  24. package/src/components/session-utils.ts +337 -0
  25. package/src/components/theme.ts +206 -0
  26. package/src/components/types.ts +215 -0
  27. package/src/config/defaults.ts +44 -0
  28. package/src/config/env.ts +67 -0
  29. package/src/config/global.ts +252 -0
  30. package/src/config/index.ts +171 -0
  31. package/src/config/types.ts +131 -0
  32. package/src/core/.gitkeep +0 -0
  33. package/src/core/index.ts +5 -0
  34. package/src/core/parser.ts +62 -0
  35. package/src/core/process-manager.ts +52 -0
  36. package/src/core/session.ts +423 -0
  37. package/src/core/tmux.ts +206 -0
  38. package/src/git/.gitkeep +0 -0
  39. package/src/git/index.ts +8 -0
  40. package/src/git/repo.ts +443 -0
  41. package/src/git/worktree.ts +317 -0
  42. package/src/github/.gitkeep +0 -0
  43. package/src/github/client.ts +208 -0
  44. package/src/github/index.ts +8 -0
  45. package/src/github/issues.ts +351 -0
  46. package/src/index.ts +369 -0
  47. package/src/prompts/.gitkeep +0 -0
  48. package/src/prompts/index.ts +1 -0
  49. package/src/prompts/swe-system.ts +22 -0
  50. package/src/providers/claude.ts +103 -0
  51. package/src/providers/index.ts +21 -0
  52. package/src/providers/opencode.ts +98 -0
  53. package/src/providers/registry.ts +53 -0
  54. package/src/providers/types.ts +117 -0
  55. package/src/store/buffers.ts +234 -0
  56. package/src/store/db.test.ts +579 -0
  57. package/src/store/db.ts +249 -0
  58. package/src/store/index.ts +101 -0
  59. package/src/store/project.ts +119 -0
  60. package/src/store/schema.sql +71 -0
  61. package/src/store/sessions.ts +454 -0
  62. package/src/store/types.ts +194 -0
  63. package/src/theme/context.tsx +170 -0
  64. package/src/theme/custom.ts +134 -0
  65. package/src/theme/index.ts +58 -0
  66. package/src/theme/loader.ts +264 -0
  67. package/src/theme/themes/aura.json +69 -0
  68. package/src/theme/themes/ayu.json +80 -0
  69. package/src/theme/themes/carbonfox.json +248 -0
  70. package/src/theme/themes/catppuccin-frappe.json +233 -0
  71. package/src/theme/themes/catppuccin-macchiato.json +233 -0
  72. package/src/theme/themes/catppuccin.json +112 -0
  73. package/src/theme/themes/cobalt2.json +228 -0
  74. package/src/theme/themes/cursor.json +249 -0
  75. package/src/theme/themes/dracula.json +219 -0
  76. package/src/theme/themes/everforest.json +241 -0
  77. package/src/theme/themes/flexoki.json +237 -0
  78. package/src/theme/themes/github.json +233 -0
  79. package/src/theme/themes/gruvbox.json +242 -0
  80. package/src/theme/themes/kanagawa.json +77 -0
  81. package/src/theme/themes/lucent-orng.json +237 -0
  82. package/src/theme/themes/material.json +235 -0
  83. package/src/theme/themes/matrix.json +77 -0
  84. package/src/theme/themes/mercury.json +252 -0
  85. package/src/theme/themes/monokai.json +221 -0
  86. package/src/theme/themes/nightowl.json +221 -0
  87. package/src/theme/themes/nord.json +223 -0
  88. package/src/theme/themes/one-dark.json +84 -0
  89. package/src/theme/themes/opencode.json +245 -0
  90. package/src/theme/themes/orng.json +249 -0
  91. package/src/theme/themes/osaka-jade.json +93 -0
  92. package/src/theme/themes/palenight.json +222 -0
  93. package/src/theme/themes/rosepine.json +234 -0
  94. package/src/theme/themes/solarized.json +223 -0
  95. package/src/theme/themes/synthwave84.json +226 -0
  96. package/src/theme/themes/tokyonight.json +243 -0
  97. package/src/theme/themes/vercel.json +245 -0
  98. package/src/theme/themes/vesper.json +218 -0
  99. package/src/theme/themes/zenburn.json +223 -0
  100. package/src/theme/types.ts +225 -0
  101. package/src/types/sql.d.ts +4 -0
  102. package/src/utils/ansi-parser.ts +225 -0
  103. package/src/utils/format.ts +46 -0
  104. package/src/utils/id.ts +15 -0
  105. package/src/utils/logger.ts +112 -0
  106. package/src/utils/prerequisites.ts +118 -0
  107. package/src/utils/shell.ts +9 -0
  108. package/src/wizard/flows.ts +419 -0
  109. package/src/wizard/index.ts +37 -0
  110. package/src/wizard/prompts.ts +190 -0
  111. package/src/workspace/detect.test.ts +51 -0
  112. package/src/workspace/detect.ts +223 -0
  113. package/src/workspace/index.ts +71 -0
  114. package/src/workspace/init.ts +131 -0
  115. package/src/workspace/paths.ts +143 -0
  116. package/src/workspace/project.ts +164 -0
  117. package/tsconfig.json +22 -0
package/src/app.tsx ADDED
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Main TUI application entry point
3
+ *
4
+ * Provides startTUI function to launch the OpenSWE terminal interface.
5
+ */
6
+
7
+ import { render } from "@opentui/solid"
8
+ import { createCliRenderer } from "@opentui/core"
9
+ import { App } from "./components"
10
+ import { ThemeProvider } from "./theme"
11
+ import type { GlobalConfig } from "./config"
12
+ import { createProject, initDatabaseWithPath, projectExists } from "./store"
13
+ import { getStateDatabasePath } from "./workspace/paths"
14
+ import { loadProjectConfig } from "./workspace/project"
15
+ import { logger } from "./utils/logger"
16
+
17
+ /**
18
+ * Start the TUI application
19
+ *
20
+ * @param config - Global configuration
21
+ * @param projectRoot - Absolute path to the project root directory
22
+ */
23
+ export async function startTUI(config: GlobalConfig, projectRoot: string) {
24
+ // Initialize database with project-specific path
25
+ const dbPath = getStateDatabasePath(projectRoot)
26
+ await initDatabaseWithPath(dbPath)
27
+
28
+ // Ensure project metadata exists in the database
29
+ try {
30
+ const projectConfig = await loadProjectConfig(projectRoot)
31
+ if (!projectConfig) {
32
+ logger.warn("Project config not found; skipping project DB initialization")
33
+ } else if (!projectExists()) {
34
+ createProject({
35
+ repoFullName: projectConfig.repoFullName,
36
+ repoUrl: projectConfig.repoUrl,
37
+ })
38
+ logger.debug("Initialized project state in database", projectConfig)
39
+ } else {
40
+ logger.debug("Project state already present in database")
41
+ }
42
+ } catch (error) {
43
+ logger.error("Failed to initialize project state in database", error)
44
+ }
45
+
46
+ // Render the TUI
47
+ const rendererConfig = {
48
+ stdin: process.stdin,
49
+ stdout: process.stdout,
50
+ useAlternateScreen: true,
51
+ useMouse: false,
52
+ useKittyKeyboard:
53
+ process.env.OPENTUI_KITTY === "1"
54
+ ? {
55
+ disambiguate: true,
56
+ alternateKeys: true,
57
+ events: true,
58
+ }
59
+ : null,
60
+ }
61
+
62
+ process.stdin.resume()
63
+
64
+ const renderer = await createCliRenderer(rendererConfig)
65
+
66
+ // Wait for destroy event to know when to exit
67
+ const exitPromise = new Promise<void>((resolve) => {
68
+ renderer.on("destroy", () => resolve())
69
+ })
70
+
71
+ await render(
72
+ () => (
73
+ <ThemeProvider
74
+ initialTheme={config.ui?.theme}
75
+ initialMode={config.ui?.themeMode}
76
+ >
77
+ <App config={config} projectRoot={projectRoot} />
78
+ </ThemeProvider>
79
+ ),
80
+ renderer,
81
+ )
82
+
83
+ await exitPromise
84
+ }
@@ -0,0 +1,526 @@
1
+ /**
2
+ * App component - root component for OpenSWE TUI
3
+ *
4
+ * Manages:
5
+ * - State signals for sessions, selection, project info
6
+ * - Data loading from store
7
+ * - Keyboard navigation
8
+ * - Layout structure
9
+ */
10
+
11
+ import { createSignal, createEffect, onMount, onCleanup, Show } from "solid-js"
12
+ import { useKeyboard, useRenderer } from "@opentui/solid"
13
+ import type { Session } from "../store"
14
+ import {
15
+ getAllSessions,
16
+ getProject,
17
+ updateSessionStatus,
18
+ getSession,
19
+ } from "../store"
20
+ import { SessionManager } from "../core"
21
+ import { generateSWEPrompt } from "../prompts"
22
+ import type { AppProps, ModalType, ProjectInfo, PendingAction } from "./types"
23
+ import type { ProviderBranding } from "../providers"
24
+ import type { AIBackend } from "../config"
25
+ import { saveGlobalConfig } from "../config"
26
+ import { StatusBar } from "./StatusBar"
27
+ import { SessionList } from "./SessionList"
28
+ import { Preview } from "./Preview"
29
+ import { HelpModal } from "./HelpModal"
30
+ import { ConfirmDialog } from "./ConfirmDialog"
31
+ import { IssueSelectorModal } from "./IssueSelectorModal"
32
+ import { ManualSessionModal } from "./ManualSessionModal"
33
+ import { ProviderSwitcherModal } from "./ProviderSwitcherModal"
34
+ import { ThemeSwitcherModal } from "./ThemeSwitcherModal"
35
+ import { deleteSessionWithWorktree } from "./session-utils"
36
+ import { useColors } from "./theme"
37
+ import { useTheme } from "../theme"
38
+ import { logger } from "../utils/logger"
39
+
40
+ /** Refresh interval for polling data (24 fps) */
41
+ const REFRESH_INTERVAL = (1/60) * 1000
42
+
43
+ export function App(props: AppProps) {
44
+ const sessionManager = new SessionManager(props.config, props.projectRoot)
45
+ const renderer = useRenderer()
46
+ const { themeName, setTheme } = useTheme()
47
+ const colors = useColors()
48
+
49
+ // Get provider branding from session manager (reactive)
50
+ const [providerBranding, setProviderBranding] = createSignal<ProviderBranding>(
51
+ sessionManager.getProvider().branding
52
+ )
53
+
54
+ // ============================================================================
55
+ // State
56
+ // ============================================================================
57
+
58
+ const [sessions, setSessions] = createSignal<Session[]>([])
59
+ const [selectedIndex, setSelectedIndex] = createSignal(0)
60
+ const [projectInfo, setProjectInfo] = createSignal<ProjectInfo | null>(null)
61
+ const [snapshotLines, setSnapshotLines] = createSignal<string[]>([])
62
+ const [sessionStartedAt, setSessionStartedAt] = createSignal<Date | undefined>(undefined)
63
+ const [activeModal, setActiveModal] = createSignal<ModalType>("none")
64
+ const [pendingAction, setPendingAction] = createSignal<PendingAction | null>(null)
65
+ const [isAttaching, setIsAttaching] = createSignal(false)
66
+
67
+ // Terminal size tracking
68
+ const [termSize, setTermSize] = createSignal({ cols: process.stdout.columns, rows: process.stdout.rows })
69
+
70
+ // ============================================================================
71
+ // Derived State
72
+ // ============================================================================
73
+
74
+ const selectedSession = (): Session | null => {
75
+ const sessionList = sessions()
76
+ const index = selectedIndex()
77
+ if (index >= 0 && index < sessionList.length) {
78
+ return sessionList[index] ?? null
79
+ }
80
+ return null
81
+ }
82
+
83
+ const sessionListWidth = () => Math.floor(termSize().cols * 0.3)
84
+
85
+ // Calculate optimal terminal size for preview
86
+ const previewSize = () => {
87
+ const { cols, rows } = termSize()
88
+ // Preview is 70% width. Subtract 4 for borders/padding.
89
+ // Height is full minus header/footer status bars (2 lines) and borders (2 lines)
90
+ // We use rows - 2 to be aggressive and fill the space, allowing slight scroll if needed
91
+ // rather than showing a gap.
92
+ return {
93
+ cols: Math.max(20, Math.floor(cols * 0.7) - 4),
94
+ rows: Math.max(10, rows - 2)
95
+ }
96
+ }
97
+
98
+ // ============================================================================
99
+ // Data Loading
100
+ // ============================================================================
101
+
102
+ const loadSessions = () => {
103
+ try {
104
+ const loadedSessions = getAllSessions()
105
+ setSessions(loadedSessions)
106
+ // Clamp selectedIndex to valid range
107
+ if (loadedSessions.length > 0) {
108
+ setSelectedIndex((prev) =>
109
+ Math.min(prev, loadedSessions.length - 1)
110
+ )
111
+ } else {
112
+ setSelectedIndex(0)
113
+ }
114
+ } catch {
115
+ // Database might not be initialized
116
+ setSessions([])
117
+ }
118
+ }
119
+
120
+ const loadProjectInfo = () => {
121
+ try {
122
+ const project = getProject()
123
+ if (project) {
124
+ setProjectInfo({
125
+ repoFullName: project.repoFullName,
126
+ repoUrl: project.repoUrl,
127
+ })
128
+ logger.debug("Loaded project info", project)
129
+ } else {
130
+ logger.warn("Project info missing from database")
131
+ }
132
+ } catch {
133
+ setProjectInfo(null)
134
+ logger.warn("Failed to load project info")
135
+ }
136
+ }
137
+
138
+ // ============================================================================
139
+ // Effects
140
+ // ============================================================================
141
+
142
+ // Load initial data on mount and set up periodic refresh
143
+ onMount(() => {
144
+ // Recover any orphaned sessions from previous runs
145
+ sessionManager.recoverSessions()
146
+
147
+ loadSessions()
148
+ loadProjectInfo()
149
+
150
+ // Set up periodic refresh for dashboard data
151
+ const refreshId = setInterval(() => {
152
+ loadSessions()
153
+ }, REFRESH_INTERVAL)
154
+
155
+ // Set up faster refresh for preview activities
156
+ const previewId = setInterval(() => {
157
+ const session = selectedSession()
158
+ if (session && session.status === "active") {
159
+ sessionManager.getSnapshot(session.id).then(lines => {
160
+ setSnapshotLines(lines)
161
+ }).catch(() => setSnapshotLines([]))
162
+
163
+ // Sync terminal size if not currently attaching/attached
164
+ if (!isAttaching()) {
165
+ const size = previewSize()
166
+ sessionManager.resizeSession(session.id, size.cols, size.rows).catch(() => {})
167
+ }
168
+ }
169
+ }, REFRESH_INTERVAL)
170
+
171
+ // Handle Window Resize
172
+ const onResize = () => {
173
+ setTermSize({ cols: process.stdout.columns, rows: process.stdout.rows })
174
+ }
175
+ process.on('SIGWINCH', onResize)
176
+
177
+ onCleanup(() => {
178
+ clearInterval(refreshId)
179
+ clearInterval(previewId)
180
+ process.off('SIGWINCH', onResize)
181
+ })
182
+ })
183
+
184
+ // Load preview activities when selection changes
185
+ createEffect(() => {
186
+ const session = selectedSession()
187
+ if (session) {
188
+ // Track session start time for duration display
189
+ if (session.status === "active" && session.createdAt) {
190
+ setSessionStartedAt(new Date(session.createdAt))
191
+
192
+ // Resize immediately on selection, but only if not attaching
193
+ if (!isAttaching()) {
194
+ const size = previewSize()
195
+ sessionManager.resizeSession(session.id, size.cols, size.rows).catch(() => {})
196
+ }
197
+ } else {
198
+ setSessionStartedAt(undefined)
199
+ }
200
+ } else {
201
+ setSessionStartedAt(undefined)
202
+ }
203
+ })
204
+
205
+ // ============================================================================
206
+ // Keyboard Handling
207
+ // ============================================================================
208
+
209
+ useKeyboard((event) => {
210
+ const modal = activeModal()
211
+
212
+ // Handle modal-specific keys
213
+ if (modal === "confirm-delete") {
214
+ switch (event.name) {
215
+ case "escape":
216
+ setPendingAction(null)
217
+ setActiveModal("none")
218
+ break
219
+
220
+ case "return": {
221
+ const action = pendingAction()
222
+ if (action && action.type === "delete") {
223
+ deleteSessionWithWorktree(props.projectRoot, action.sessionId).then(() => {
224
+ setPendingAction(null)
225
+ setActiveModal("none")
226
+ loadSessions()
227
+ })
228
+ }
229
+ break
230
+ }
231
+ }
232
+ return
233
+ }
234
+
235
+ if (modal !== "none") {
236
+ return
237
+ }
238
+
239
+ const sessionList = sessions()
240
+
241
+ switch (event.name) {
242
+ // Navigation
243
+ case "j":
244
+ case "down":
245
+ if (sessionList.length > 0) {
246
+ setSelectedIndex((prev) =>
247
+ Math.min(prev + 1, sessionList.length - 1)
248
+ )
249
+ }
250
+ break
251
+
252
+ case "k":
253
+ case "up":
254
+ if (sessionList.length > 0) {
255
+ setSelectedIndex((prev) => Math.max(prev - 1, 0))
256
+ }
257
+ break
258
+
259
+ // Session actions
260
+ case "n":
261
+ setActiveModal("manual")
262
+ break
263
+
264
+ case "d": {
265
+ const session = selectedSession()
266
+ if (session) {
267
+ setPendingAction({
268
+ type: "delete",
269
+ sessionId: session.id,
270
+ sessionName: session.name,
271
+ })
272
+ setActiveModal("confirm-delete")
273
+ }
274
+ break
275
+ }
276
+
277
+ case "p": {
278
+ const session = selectedSession()
279
+ if (session) {
280
+ if (session.status === "paused") {
281
+ updateSessionStatus(session.id, "queued")
282
+ } else if (session.status === "active" || session.status === "queued") {
283
+ updateSessionStatus(session.id, "paused")
284
+ }
285
+ loadSessions()
286
+ }
287
+ break
288
+ }
289
+
290
+ case "r":
291
+ loadSessions()
292
+ break
293
+
294
+ case "return":
295
+ // Full takeover mode - start session if needed, then attach
296
+ (async () => {
297
+ const session = selectedSession()
298
+ if (!session) return
299
+
300
+ setIsAttaching(true)
301
+
302
+ try {
303
+ // If session needs starting, start it first and await completion
304
+ if (session.status === "queued" || session.status === "paused") {
305
+ await sessionManager.startSession({
306
+ sessionId: session.id,
307
+ prompt: session.issueNumber ? generateSWEPrompt(session) : undefined,
308
+ resumeSessionId: session.aiSessionData?.sessionId,
309
+ })
310
+ }
311
+
312
+ // Re-fetch session to get updated status after start
313
+ const updatedSession = getSession(session.id)
314
+ if (updatedSession?.status === "active") {
315
+ // Resize to full terminal size for interactive use
316
+ await sessionManager.resizeSession(session.id, termSize().cols, termSize().rows)
317
+
318
+ const cmd = sessionManager.getAttachCommand(session.id)
319
+
320
+ // Suspend OpenTUI before yielding terminal to tmux
321
+ renderer.suspend()
322
+
323
+ Bun.spawnSync(cmd, { stdio: ["inherit", "inherit", "inherit"] })
324
+
325
+ // Resume OpenTUI - this properly re-establishes terminal state
326
+ renderer.resume()
327
+
328
+ // Resize back to preview size
329
+ const size = previewSize()
330
+ await sessionManager.resizeSession(session.id, size.cols, size.rows)
331
+ }
332
+
333
+ loadSessions()
334
+ } catch (error) {
335
+ logger.error("Session start/attach failed:", error)
336
+ loadSessions()
337
+ } finally {
338
+ setIsAttaching(false)
339
+ }
340
+ })()
341
+ break
342
+
343
+ // Modal triggers
344
+ case "i":
345
+ if (projectInfo()) {
346
+ setActiveModal("issues")
347
+ } else {
348
+ logger.warn("Cannot open issues modal: project info not loaded")
349
+ }
350
+ break
351
+
352
+ case "?":
353
+ setActiveModal("help")
354
+ break
355
+
356
+ case "a":
357
+ setActiveModal("provider")
358
+ break
359
+
360
+ case "t":
361
+ setActiveModal("theme")
362
+ break
363
+
364
+ // Quit
365
+ case "q":
366
+ renderer.destroy()
367
+ break
368
+ }
369
+ })
370
+
371
+ // ============================================================================
372
+ // Selection Handler
373
+ // ============================================================================
374
+
375
+ const handleSelect = (index: number) => {
376
+ setSelectedIndex(index)
377
+ }
378
+
379
+ // ============================================================================
380
+ // Provider Change Handler
381
+ // ============================================================================
382
+
383
+ const handleProviderChange = async (backend: AIBackend) => {
384
+ sessionManager.setProvider(backend)
385
+ setProviderBranding(sessionManager.getProvider().branding)
386
+ setActiveModal("none")
387
+
388
+ // Persist to config file
389
+ try {
390
+ await saveGlobalConfig({ ai: { backend } })
391
+ } catch (error) {
392
+ logger.error("Failed to save provider preference:", error)
393
+ }
394
+ }
395
+
396
+ // ============================================================================
397
+ // Theme Change Handler
398
+ // ============================================================================
399
+
400
+ const handleThemeChange = async (name: string) => {
401
+ setTheme(name)
402
+ setActiveModal("none")
403
+
404
+ // Persist to config file
405
+ try {
406
+ await saveGlobalConfig({ ui: { theme: name } })
407
+ } catch (error) {
408
+ logger.error("Failed to save theme preference:", error)
409
+ }
410
+ }
411
+
412
+ // ============================================================================
413
+ // Render
414
+ // ============================================================================
415
+
416
+ return (
417
+ <box
418
+ flexDirection="column"
419
+ width="100%"
420
+ height="100%"
421
+ backgroundColor={colors().bg.primary}
422
+ >
423
+ {/* Header Status Bar */}
424
+ <StatusBar
425
+ variant="header"
426
+ repoName={projectInfo()?.repoFullName}
427
+ backend={props.config.ai.backend}
428
+ sessionId={selectedSession()?.id}
429
+ providerBranding={providerBranding()}
430
+ />
431
+
432
+ {/* Main Content Area */}
433
+ <box flexDirection="row" flexGrow={1}>
434
+ <SessionList
435
+ sessions={sessions()}
436
+ selectedIndex={selectedIndex()}
437
+ onSelect={handleSelect}
438
+ listWidth={sessionListWidth()}
439
+ />
440
+ <Preview
441
+ session={selectedSession()}
442
+ startedAt={sessionStartedAt()}
443
+ snapshotLines={snapshotLines()}
444
+ providerBranding={providerBranding()}
445
+ />
446
+ </box>
447
+
448
+ {/* Footer Status Bar */}
449
+ <StatusBar variant="footer" />
450
+
451
+ {/* Modal Overlays */}
452
+ <Show when={activeModal() === "help"}>
453
+ <HelpModal onClose={() => setActiveModal("none")} />
454
+ </Show>
455
+
456
+ <Show when={activeModal() === "confirm-delete" && pendingAction()}>
457
+ <ConfirmDialog
458
+ title="Delete Session?"
459
+ message={`Are you sure you want to delete\n"${pendingAction()!.sessionName}"?\n\nThis will also remove the worktree.`}
460
+ onConfirm={async () => {
461
+ const action = pendingAction()
462
+ if (action) {
463
+ await deleteSessionWithWorktree(props.projectRoot, action.sessionId)
464
+ }
465
+ setPendingAction(null)
466
+ setActiveModal("none")
467
+ loadSessions()
468
+ }}
469
+ onCancel={() => {
470
+ setPendingAction(null)
471
+ setActiveModal("none")
472
+ }}
473
+ />
474
+ </Show>
475
+
476
+ <Show when={activeModal() === "issues" && projectInfo()}>
477
+ <IssueSelectorModal
478
+ ownerRepo={projectInfo()!.repoFullName}
479
+ projectRoot={props.projectRoot}
480
+ currentBackend={props.config.ai.backend}
481
+ onClose={() => setActiveModal("none")}
482
+ onSessionsCreated={async (sessions) => {
483
+ // Auto-start all created sessions
484
+ for (const session of sessions) {
485
+ try {
486
+ await sessionManager.startSession({
487
+ sessionId: session.id,
488
+ prompt: generateSWEPrompt(session),
489
+ })
490
+ } catch (error) {
491
+ logger.error("Failed to auto-start session", { sessionId: session.id, error })
492
+ }
493
+ }
494
+ loadSessions()
495
+ }}
496
+ />
497
+ </Show>
498
+
499
+ <Show when={activeModal() === "manual"}>
500
+ <ManualSessionModal
501
+ projectRoot={props.projectRoot}
502
+ currentBackend={props.config.ai.backend}
503
+ sessionManager={sessionManager}
504
+ onClose={() => setActiveModal("none")}
505
+ onSessionCreated={() => loadSessions()}
506
+ />
507
+ </Show>
508
+
509
+ <Show when={activeModal() === "provider"}>
510
+ <ProviderSwitcherModal
511
+ currentBackend={props.config.ai.backend}
512
+ onSelect={handleProviderChange}
513
+ onClose={() => setActiveModal("none")}
514
+ />
515
+ </Show>
516
+
517
+ <Show when={activeModal() === "theme"}>
518
+ <ThemeSwitcherModal
519
+ currentTheme={themeName()}
520
+ onSelect={handleThemeChange}
521
+ onClose={() => setActiveModal("none")}
522
+ />
523
+ </Show>
524
+ </box>
525
+ )
526
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * ConfirmDialog component - Reusable confirmation dialog
3
+ *
4
+ * Used for destructive actions like deleting sessions.
5
+ */
6
+
7
+ import { For } from "solid-js"
8
+ import { Footer } from "./Footer"
9
+ import { ScrollableText } from "./ScrollableText"
10
+ import type { ConfirmDialogProps } from "./types"
11
+ import { useColors } from "./theme"
12
+
13
+ // Bold attribute constant
14
+ const BOLD = 1
15
+
16
+ export function ConfirmDialog(props: ConfirmDialogProps) {
17
+ const colors = useColors()
18
+ const modalWidth = 45
19
+
20
+ // Split message into lines
21
+ const messageLines = () => props.message.split("\n")
22
+ // Height: title(1) + message lines + padding(2) + footer(1) + borders(2)
23
+ const modalHeight = () => messageLines().length + 6
24
+
25
+ return (
26
+ <box
27
+ position="absolute"
28
+ top={0}
29
+ left={0}
30
+ width="100%"
31
+ height="100%"
32
+ justifyContent="center"
33
+ alignItems="center"
34
+ >
35
+ {/* Modal container */}
36
+ <box
37
+ flexDirection="column"
38
+ width={modalWidth}
39
+ height={modalHeight()}
40
+ backgroundColor={colors().bg.secondary}
41
+ borderStyle="rounded"
42
+ borderColor={colors().border.accent}
43
+ overflow="hidden"
44
+ >
45
+ {/* Header / Title */}
46
+ <box
47
+ height={1}
48
+ justifyContent="center"
49
+ paddingLeft={1}
50
+ paddingRight={1}
51
+ >
52
+ <text fg={colors().text.primary} attributes={BOLD}>
53
+ {props.title}
54
+ </text>
55
+ </box>
56
+
57
+ {/* Message content */}
58
+ <box
59
+ flexDirection="column"
60
+ flexGrow={1}
61
+ paddingLeft={2}
62
+ paddingRight={2}
63
+ paddingTop={1}
64
+ paddingBottom={1}
65
+ >
66
+ <For each={messageLines()}>
67
+ {(line) => (
68
+ <ScrollableText
69
+ text={line || " "}
70
+ width={38}
71
+ fg={colors().text.primary}
72
+ />
73
+ )}
74
+ </For>
75
+ </box>
76
+
77
+ {/* Footer with action hints */}
78
+ <Footer
79
+ actions={[
80
+ { key: "Enter", label: "Confirm" },
81
+ { key: "Esc", label: "Cancel" },
82
+ ]}
83
+ bgColor={colors().bg.primary}
84
+ />
85
+ </box>
86
+ </box>
87
+ )
88
+ }