@cat-factory/app 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +88 -0
  3. package/app/app.config.ts +8 -0
  4. package/app/app.vue +11 -0
  5. package/app/assets/css/main.css +100 -0
  6. package/app/components/auth/AuthGate.vue +24 -0
  7. package/app/components/auth/LoginScreen.vue +18 -0
  8. package/app/components/auth/UserMenu.vue +39 -0
  9. package/app/components/board/AgentFailureCard.vue +97 -0
  10. package/app/components/board/AgentStopButton.vue +61 -0
  11. package/app/components/board/BoardCanvas.vue +146 -0
  12. package/app/components/board/TaskDependencyEdges.vue +132 -0
  13. package/app/components/board/nodes/AgentChip.vue +59 -0
  14. package/app/components/board/nodes/BlockNode.vue +347 -0
  15. package/app/components/board/nodes/DecisionBadge.vue +21 -0
  16. package/app/components/board/nodes/DraggableTask.vue +69 -0
  17. package/app/components/board/nodes/ModuleFrame.vue +70 -0
  18. package/app/components/board/nodes/TaskCard.vue +237 -0
  19. package/app/components/bootstrap/BootstrapModal.vue +665 -0
  20. package/app/components/documents/DocumentImportModal.vue +161 -0
  21. package/app/components/documents/DocumentSourceConnectModal.vue +127 -0
  22. package/app/components/documents/SpawnPreviewModal.vue +161 -0
  23. package/app/components/documents/TaskContextDocs.vue +83 -0
  24. package/app/components/focus/BlockFocusView.vue +161 -0
  25. package/app/components/fragments/FragmentLibraryPanel.vue +340 -0
  26. package/app/components/github/GitHubConnect.vue +183 -0
  27. package/app/components/github/GitHubPanel.vue +584 -0
  28. package/app/components/layout/BoardSwitcher.vue +202 -0
  29. package/app/components/layout/BoardToolbar.vue +109 -0
  30. package/app/components/layout/SideBar.vue +193 -0
  31. package/app/components/layout/SpendWarningBanner.vue +107 -0
  32. package/app/components/palettes/AgentPalette.vue +33 -0
  33. package/app/components/palettes/BlockPalette.vue +41 -0
  34. package/app/components/palettes/PipelinePalette.vue +74 -0
  35. package/app/components/panels/DecisionModal.vue +71 -0
  36. package/app/components/panels/InspectorPanel.vue +296 -0
  37. package/app/components/panels/inspector/ContainerSummary.vue +74 -0
  38. package/app/components/panels/inspector/TaskDependencies.vue +70 -0
  39. package/app/components/panels/inspector/TaskExecution.vue +175 -0
  40. package/app/components/panels/inspector/TaskModelSettings.vue +128 -0
  41. package/app/components/panels/inspector/TaskStructure.vue +139 -0
  42. package/app/components/pipeline/PipelineBuilder.vue +227 -0
  43. package/app/components/pipeline/PipelineProgress.vue +246 -0
  44. package/app/components/requirements/RequirementReviewModal.vue +328 -0
  45. package/app/components/scenarios/FeatureScenarios.vue +162 -0
  46. package/app/components/scenarios/ScenarioCard.vue +109 -0
  47. package/app/components/tasks/TaskContextIssues.vue +88 -0
  48. package/app/components/tasks/TaskImportModal.vue +140 -0
  49. package/app/components/tasks/TaskSourceConnectModal.vue +122 -0
  50. package/app/composables/useApi.ts +535 -0
  51. package/app/composables/useBlockDrag.ts +75 -0
  52. package/app/composables/useBlockQueries.ts +136 -0
  53. package/app/composables/useBoardFlow.ts +11 -0
  54. package/app/composables/useDepLabels.ts +26 -0
  55. package/app/composables/useSemanticZoom.ts +16 -0
  56. package/app/composables/useWorkspaceStream.ts +125 -0
  57. package/app/docs/architecture.md +31 -0
  58. package/app/pages/index.vue +80 -0
  59. package/app/stores/accounts.ts +64 -0
  60. package/app/stores/agentRuns.ts +117 -0
  61. package/app/stores/agents.ts +40 -0
  62. package/app/stores/auth.ts +97 -0
  63. package/app/stores/board.spec.ts +197 -0
  64. package/app/stores/board.ts +147 -0
  65. package/app/stores/bootstrap.ts +97 -0
  66. package/app/stores/documents.ts +165 -0
  67. package/app/stores/execution.ts +115 -0
  68. package/app/stores/fragmentLibrary.ts +147 -0
  69. package/app/stores/fragments.ts +40 -0
  70. package/app/stores/github.ts +291 -0
  71. package/app/stores/models.ts +48 -0
  72. package/app/stores/pipelines.ts +77 -0
  73. package/app/stores/requirements.ts +133 -0
  74. package/app/stores/scenarios.spec.ts +82 -0
  75. package/app/stores/scenarios.ts +196 -0
  76. package/app/stores/tasks.spec.ts +71 -0
  77. package/app/stores/tasks.ts +149 -0
  78. package/app/stores/ui.ts +204 -0
  79. package/app/stores/workspace.ts +201 -0
  80. package/app/types/accounts.ts +38 -0
  81. package/app/types/bootstrap.ts +83 -0
  82. package/app/types/documents.ts +92 -0
  83. package/app/types/domain.ts +216 -0
  84. package/app/types/execution.ts +110 -0
  85. package/app/types/fragments.ts +72 -0
  86. package/app/types/github.ts +153 -0
  87. package/app/types/models.ts +48 -0
  88. package/app/types/requirements.ts +38 -0
  89. package/app/types/scenarios.ts +36 -0
  90. package/app/types/tasks.ts +67 -0
  91. package/app/utils/catalog.spec.ts +82 -0
  92. package/app/utils/catalog.ts +185 -0
  93. package/app/utils/dnd.ts +29 -0
  94. package/nuxt.config.ts +43 -0
  95. package/package.json +43 -0
@@ -0,0 +1,584 @@
1
+ <script setup lang="ts">
2
+ // GitHub integration panel: connect the workspace's App installation, manage the
3
+ // connection (disconnect / resync), and browse the projected repos, branches,
4
+ // pull requests and issues the backend caches in D1. Mirrors the document-source
5
+ // connect/import surface, but for GitHub. Writes (new branch, open/merge PR,
6
+ // comment) go straight to the repo via the backend's installation token.
7
+ import type { GitHubPullRequest, GitHubRepo } from '~/types/domain'
8
+ // Explicit import: the auto-import name for a component nested under a
9
+ // like-named directory (github/GitHubConnect) doesn't match the `<GitHubConnect>`
10
+ // tag, so it silently renders as an empty element. Importing it directly binds
11
+ // the tag unambiguously.
12
+ import GitHubConnect from './GitHubConnect.vue'
13
+
14
+ const ui = useUiStore()
15
+ const github = useGitHubStore()
16
+ const toast = useToast()
17
+
18
+ const open = computed({
19
+ get: () => ui.githubOpen,
20
+ set: (v: boolean) => {
21
+ if (!v) ui.closeGitHub()
22
+ },
23
+ })
24
+
25
+ // On open: refresh projections when connected. The not-connected state renders
26
+ // <GitHubConnect>, which discovers and links installations on its own.
27
+ watch(open, (isOpen) => {
28
+ if (!isOpen) return
29
+ if (github.connected) void github.load()
30
+ })
31
+
32
+ function notifyError(title: string, e: unknown) {
33
+ toast.add({
34
+ title,
35
+ description: e instanceof Error ? e.message : String(e),
36
+ icon: 'i-lucide-triangle-alert',
37
+ color: 'error',
38
+ })
39
+ }
40
+
41
+ async function disconnect() {
42
+ try {
43
+ await github.disconnect()
44
+ toast.add({ title: 'GitHub disconnected', icon: 'i-lucide-unplug' })
45
+ } catch (e) {
46
+ notifyError('Could not disconnect', e)
47
+ }
48
+ }
49
+
50
+ async function resync(full = false) {
51
+ try {
52
+ const { status } = await github.resync({ full })
53
+ toast.add({ title: `Resync ${status}`, icon: 'i-lucide-refresh-cw', color: 'info' })
54
+ } catch (e) {
55
+ notifyError('Could not resync', e)
56
+ }
57
+ }
58
+
59
+ // ---- browse ----------------------------------------------------------------
60
+ type Tab = 'repos' | 'pulls' | 'issues'
61
+ const tab = ref<Tab>('repos')
62
+ const tabs: { id: Tab; label: string; icon: string }[] = [
63
+ { id: 'repos', label: 'Repositories', icon: 'i-lucide-folder-git-2' },
64
+ { id: 'pulls', label: 'Pull requests', icon: 'i-lucide-git-pull-request' },
65
+ { id: 'issues', label: 'Issues', icon: 'i-lucide-circle-dot' },
66
+ ]
67
+
68
+ // Manage which repos this board links (the installation is shared across the
69
+ // account; each board picks its own repos).
70
+ const managing = ref(false)
71
+ const selected = ref<Set<number>>(new Set())
72
+
73
+ async function openManage() {
74
+ managing.value = true
75
+ try {
76
+ await github.loadAvailableRepos()
77
+ selected.value = new Set(github.availableRepos.filter((r) => r.linked).map((r) => r.githubId))
78
+ } catch (e) {
79
+ notifyError('Could not load repositories', e)
80
+ managing.value = false
81
+ }
82
+ }
83
+
84
+ function toggleSelected(githubId: number) {
85
+ const next = new Set(selected.value)
86
+ if (next.has(githubId)) next.delete(githubId)
87
+ else next.add(githubId)
88
+ selected.value = next
89
+ }
90
+
91
+ async function saveRepos() {
92
+ try {
93
+ await github.setLinkedRepos([...selected.value])
94
+ managing.value = false
95
+ toast.add({ title: 'Linked repositories updated', icon: 'i-lucide-check', color: 'success' })
96
+ } catch (e) {
97
+ notifyError('Could not update repositories', e)
98
+ }
99
+ }
100
+
101
+ // Repos: expand to load branches + open an inline "new branch" form.
102
+ const expandedRepo = ref<number | null>(null)
103
+ const branchForm = ref<{ name: string; fromSha: string }>({ name: '', fromSha: '' })
104
+ const creatingBranch = ref(false)
105
+
106
+ async function toggleRepo(repo: GitHubRepo) {
107
+ if (expandedRepo.value === repo.githubId) {
108
+ expandedRepo.value = null
109
+ return
110
+ }
111
+ expandedRepo.value = repo.githubId
112
+ branchForm.value = { name: '', fromSha: '' }
113
+ if (!github.branches[repo.githubId]) {
114
+ try {
115
+ await github.loadBranches(repo.githubId)
116
+ } catch (e) {
117
+ notifyError('Could not load branches', e)
118
+ }
119
+ }
120
+ }
121
+
122
+ async function createBranch(repo: GitHubRepo) {
123
+ const name = branchForm.value.name.trim()
124
+ const fromSha = branchForm.value.fromSha.trim()
125
+ if (!name || !fromSha) return
126
+ creatingBranch.value = true
127
+ try {
128
+ await github.createBranch(repo.githubId, { name, fromSha })
129
+ branchForm.value = { name: '', fromSha: '' }
130
+ toast.add({ title: `Branch ${name} created`, icon: 'i-lucide-check', color: 'success' })
131
+ } catch (e) {
132
+ notifyError('Could not create branch', e)
133
+ } finally {
134
+ creatingBranch.value = false
135
+ }
136
+ }
137
+
138
+ // Pull requests: open a new PR + merge an existing open one.
139
+ const prForm = ref<{ repoGithubId: number | null; title: string; head: string; base: string }>({
140
+ repoGithubId: null,
141
+ title: '',
142
+ head: '',
143
+ base: '',
144
+ })
145
+ const showPrForm = ref(false)
146
+ const openingPr = ref(false)
147
+
148
+ const repoMenu = computed(() => [
149
+ github.repos.map((r) => ({
150
+ label: `${r.owner}/${r.name}`,
151
+ icon: 'i-lucide-folder-git-2',
152
+ onSelect: () => {
153
+ prForm.value.repoGithubId = r.githubId
154
+ if (!prForm.value.base) prForm.value.base = r.defaultBranch ?? ''
155
+ },
156
+ })),
157
+ ])
158
+ const prRepo = computed(() => github.repos.find((r) => r.githubId === prForm.value.repoGithubId))
159
+ const canOpenPr = computed(
160
+ () =>
161
+ !!prForm.value.repoGithubId &&
162
+ prForm.value.title.trim() &&
163
+ prForm.value.head.trim() &&
164
+ prForm.value.base.trim(),
165
+ )
166
+
167
+ async function openPr() {
168
+ if (!canOpenPr.value) return
169
+ openingPr.value = true
170
+ try {
171
+ await github.openPullRequest(prForm.value.repoGithubId!, {
172
+ title: prForm.value.title.trim(),
173
+ head: prForm.value.head.trim(),
174
+ base: prForm.value.base.trim(),
175
+ })
176
+ showPrForm.value = false
177
+ prForm.value = { repoGithubId: null, title: '', head: '', base: '' }
178
+ toast.add({ title: 'Pull request opened', icon: 'i-lucide-check', color: 'success' })
179
+ } catch (e) {
180
+ notifyError('Could not open pull request', e)
181
+ } finally {
182
+ openingPr.value = false
183
+ }
184
+ }
185
+
186
+ const merging = ref<number | null>(null)
187
+ async function merge(pr: GitHubPullRequest) {
188
+ merging.value = pr.number
189
+ try {
190
+ await github.mergePullRequest(pr.repoGithubId, pr.number, { method: 'squash' })
191
+ toast.add({ title: `PR #${pr.number} merged`, icon: 'i-lucide-git-merge', color: 'success' })
192
+ } catch (e) {
193
+ notifyError('Could not merge', e)
194
+ } finally {
195
+ merging.value = null
196
+ }
197
+ }
198
+ </script>
199
+
200
+ <template>
201
+ <UModal v-model:open="open" title="GitHub" :ui="{ content: 'max-w-2xl' }">
202
+ <template #body>
203
+ <div class="space-y-5">
204
+ <!-- not connected: connect -->
205
+ <template v-if="!github.connected">
206
+ <p class="text-sm text-slate-400">
207
+ Connect a GitHub App installation to back board blocks with repositories, browse pull
208
+ requests and issues, and let agents push branches and open PRs.
209
+ </p>
210
+
211
+ <GitHubConnect />
212
+ </template>
213
+
214
+ <!-- connected: manage + browse -->
215
+ <template v-else>
216
+ <!-- connection header -->
217
+ <div
218
+ class="flex items-center justify-between gap-2 rounded-md border border-slate-800 bg-slate-900/60 px-3 py-2"
219
+ >
220
+ <div class="flex items-center gap-2 min-w-0">
221
+ <UIcon name="i-lucide-github" class="h-5 w-5 text-slate-300" />
222
+ <div class="min-w-0">
223
+ <div class="truncate text-sm text-slate-200">
224
+ {{ github.connection?.accountLogin }}
225
+ </div>
226
+ <div class="text-[11px] text-slate-500">
227
+ {{ github.connection?.targetType }} · installation
228
+ {{ github.connection?.installationId }}
229
+ </div>
230
+ </div>
231
+ </div>
232
+ <div class="flex items-center gap-1">
233
+ <UButton
234
+ size="xs"
235
+ color="neutral"
236
+ variant="ghost"
237
+ icon="i-lucide-refresh-cw"
238
+ :loading="github.syncing"
239
+ @click="resync(false)"
240
+ >
241
+ Resync
242
+ </UButton>
243
+ <UButton
244
+ size="xs"
245
+ color="neutral"
246
+ variant="ghost"
247
+ icon="i-lucide-history"
248
+ :disabled="github.syncing"
249
+ @click="resync(true)"
250
+ >
251
+ Backfill
252
+ </UButton>
253
+ <UButton
254
+ size="xs"
255
+ color="error"
256
+ variant="ghost"
257
+ icon="i-lucide-unplug"
258
+ @click="disconnect"
259
+ />
260
+ </div>
261
+ </div>
262
+
263
+ <!-- tabs -->
264
+ <div class="flex gap-1">
265
+ <UButton
266
+ v-for="t in tabs"
267
+ :key="t.id"
268
+ size="sm"
269
+ :color="tab === t.id ? 'primary' : 'neutral'"
270
+ :variant="tab === t.id ? 'soft' : 'ghost'"
271
+ :icon="t.icon"
272
+ @click="tab = t.id"
273
+ >
274
+ {{ t.label }}
275
+ </UButton>
276
+ </div>
277
+
278
+ <div v-if="github.loading" class="flex items-center gap-2 py-6 text-sm text-slate-400">
279
+ <UIcon name="i-lucide-loader" class="h-4 w-4 animate-spin" /> Loading…
280
+ </div>
281
+
282
+ <!-- repositories -->
283
+ <section v-else-if="tab === 'repos'" class="space-y-2">
284
+ <!-- manage which repos this board links -->
285
+ <div class="flex items-center justify-between">
286
+ <span class="text-[11px] uppercase tracking-wide text-slate-500">
287
+ Linked to this board
288
+ </span>
289
+ <UButton
290
+ size="xs"
291
+ color="neutral"
292
+ variant="soft"
293
+ icon="i-lucide-list-checks"
294
+ @click="managing ? (managing = false) : openManage()"
295
+ >
296
+ {{ managing ? 'Close' : 'Manage repos' }}
297
+ </UButton>
298
+ </div>
299
+
300
+ <div
301
+ v-if="managing"
302
+ class="space-y-2 rounded-md border border-slate-700 bg-slate-900/80 p-3"
303
+ >
304
+ <p class="text-[12px] text-slate-400">
305
+ Pick the repositories this board should track. The GitHub connection is shared
306
+ across the account; each board links its own repos.
307
+ </p>
308
+ <div
309
+ v-if="github.loadingAvailable"
310
+ class="flex items-center gap-2 py-3 text-sm text-slate-400"
311
+ >
312
+ <UIcon name="i-lucide-loader" class="h-4 w-4 animate-spin" /> Loading repositories…
313
+ </div>
314
+ <p v-else-if="!github.availableRepos.length" class="py-2 text-sm text-slate-400">
315
+ The installation can’t access any repositories yet.
316
+ </p>
317
+ <div v-else class="max-h-64 space-y-1 overflow-y-auto">
318
+ <button
319
+ v-for="r in github.availableRepos"
320
+ :key="r.githubId"
321
+ type="button"
322
+ class="flex w-full items-center gap-2 rounded px-2 py-1.5 text-left transition hover:bg-slate-800/60"
323
+ @click="toggleSelected(r.githubId)"
324
+ >
325
+ <UIcon
326
+ :name="selected.has(r.githubId) ? 'i-lucide-check-square' : 'i-lucide-square'"
327
+ class="h-4 w-4 shrink-0"
328
+ :class="selected.has(r.githubId) ? 'text-indigo-400' : 'text-slate-500'"
329
+ />
330
+ <span class="truncate text-sm text-slate-200">{{ r.owner }}/{{ r.name }}</span>
331
+ <UBadge v-if="r.private" color="neutral" variant="subtle" size="sm">
332
+ private
333
+ </UBadge>
334
+ </button>
335
+ </div>
336
+ <div class="flex items-center justify-end gap-2 pt-1">
337
+ <UButton color="neutral" variant="ghost" size="sm" @click="managing = false">
338
+ Cancel
339
+ </UButton>
340
+ <UButton
341
+ color="primary"
342
+ size="sm"
343
+ icon="i-lucide-save"
344
+ :loading="github.savingRepos"
345
+ @click="saveRepos"
346
+ >
347
+ Save selection
348
+ </UButton>
349
+ </div>
350
+ </div>
351
+
352
+ <p v-if="!github.repos.length && !managing" class="py-4 text-sm text-slate-400">
353
+ No repositories linked yet. Use “Manage repos” to pick which repositories this board
354
+ tracks.
355
+ </p>
356
+ <div
357
+ v-for="repo in github.repos"
358
+ :key="repo.githubId"
359
+ class="rounded-md border border-slate-800 bg-slate-900/60"
360
+ >
361
+ <div class="flex items-center justify-between gap-2 px-3 py-2">
362
+ <button class="flex min-w-0 items-center gap-2 text-left" @click="toggleRepo(repo)">
363
+ <UIcon
364
+ :name="
365
+ expandedRepo === repo.githubId
366
+ ? 'i-lucide-chevron-down'
367
+ : 'i-lucide-chevron-right'
368
+ "
369
+ class="h-4 w-4 shrink-0 text-slate-500"
370
+ />
371
+ <span class="truncate text-sm text-slate-200">
372
+ {{ repo.owner }}/{{ repo.name }}
373
+ </span>
374
+ <UBadge v-if="repo.private" color="neutral" variant="subtle" size="sm">
375
+ private
376
+ </UBadge>
377
+ </button>
378
+ <div class="flex items-center gap-2">
379
+ <span v-if="repo.defaultBranch" class="text-[11px] text-slate-500">
380
+ {{ repo.defaultBranch }}
381
+ </span>
382
+ <ULink
383
+ :to="github.repoUrl(repo.githubId) ?? '#'"
384
+ target="_blank"
385
+ class="text-[11px] text-indigo-400 hover:underline"
386
+ >
387
+ Open
388
+ </ULink>
389
+ </div>
390
+ </div>
391
+
392
+ <div
393
+ v-if="expandedRepo === repo.githubId"
394
+ class="space-y-2 border-t border-slate-800 px-3 py-2"
395
+ >
396
+ <div
397
+ v-for="b in github.branches[repo.githubId] ?? []"
398
+ :key="b.name"
399
+ class="flex items-center justify-between gap-2 text-[12px]"
400
+ >
401
+ <span class="flex items-center gap-1.5 truncate text-slate-300">
402
+ <UIcon name="i-lucide-git-branch" class="h-3.5 w-3.5 text-slate-500" />
403
+ {{ b.name }}
404
+ <UBadge v-if="b.protected" color="warning" variant="subtle" size="sm">
405
+ protected
406
+ </UBadge>
407
+ </span>
408
+ <code class="text-[10px] text-slate-500">{{ b.headSha.slice(0, 7) }}</code>
409
+ </div>
410
+
411
+ <!-- new branch -->
412
+ <div class="flex items-end gap-2 pt-1">
413
+ <UFormField label="New branch" class="flex-1">
414
+ <UInput
415
+ v-model="branchForm.name"
416
+ placeholder="feature/x"
417
+ size="sm"
418
+ class="w-full"
419
+ />
420
+ </UFormField>
421
+ <UFormField label="From SHA" class="flex-1">
422
+ <UInput
423
+ v-model="branchForm.fromSha"
424
+ placeholder="commit sha"
425
+ size="sm"
426
+ class="w-full"
427
+ />
428
+ </UFormField>
429
+ <UButton
430
+ size="sm"
431
+ color="neutral"
432
+ variant="subtle"
433
+ icon="i-lucide-git-branch-plus"
434
+ :loading="creatingBranch"
435
+ :disabled="!branchForm.name.trim() || !branchForm.fromSha.trim()"
436
+ @click="createBranch(repo)"
437
+ />
438
+ </div>
439
+ </div>
440
+ </div>
441
+ </section>
442
+
443
+ <!-- pull requests -->
444
+ <section v-else-if="tab === 'pulls'" class="space-y-2">
445
+ <div class="flex justify-end">
446
+ <UButton
447
+ size="xs"
448
+ color="neutral"
449
+ variant="soft"
450
+ icon="i-lucide-plus"
451
+ @click="showPrForm = !showPrForm"
452
+ >
453
+ Open PR
454
+ </UButton>
455
+ </div>
456
+
457
+ <div
458
+ v-if="showPrForm"
459
+ class="space-y-2 rounded-md border border-slate-700 bg-slate-900/80 p-3"
460
+ >
461
+ <UFormField label="Repository">
462
+ <UDropdownMenu :items="repoMenu" :content="{ align: 'start' }">
463
+ <UButton
464
+ color="neutral"
465
+ variant="subtle"
466
+ trailing-icon="i-lucide-chevron-down"
467
+ class="w-full justify-between"
468
+ >
469
+ <span class="truncate">
470
+ {{ prRepo ? `${prRepo.owner}/${prRepo.name}` : 'Choose a repository' }}
471
+ </span>
472
+ </UButton>
473
+ </UDropdownMenu>
474
+ </UFormField>
475
+ <UFormField label="Title">
476
+ <UInput v-model="prForm.title" class="w-full" />
477
+ </UFormField>
478
+ <div class="grid grid-cols-2 gap-2">
479
+ <UFormField label="Head branch">
480
+ <UInput v-model="prForm.head" placeholder="feature/x" class="w-full" />
481
+ </UFormField>
482
+ <UFormField label="Base branch">
483
+ <UInput v-model="prForm.base" placeholder="main" class="w-full" />
484
+ </UFormField>
485
+ </div>
486
+ <div class="flex justify-end">
487
+ <UButton
488
+ color="primary"
489
+ icon="i-lucide-git-pull-request"
490
+ :loading="openingPr"
491
+ :disabled="!canOpenPr"
492
+ @click="openPr"
493
+ >
494
+ Open pull request
495
+ </UButton>
496
+ </div>
497
+ </div>
498
+
499
+ <p v-if="!github.pulls.length" class="py-4 text-sm text-slate-400">
500
+ No pull requests synced.
501
+ </p>
502
+ <div
503
+ v-for="pr in github.pulls"
504
+ :key="`${pr.repoGithubId}-${pr.number}`"
505
+ class="flex items-center justify-between gap-2 rounded-md border border-slate-800 bg-slate-900/60 px-3 py-2"
506
+ >
507
+ <div class="min-w-0">
508
+ <div class="truncate text-sm text-slate-200">
509
+ <span class="text-slate-500">#{{ pr.number }}</span> {{ pr.title }}
510
+ </div>
511
+ <div class="truncate text-[11px] text-slate-500">
512
+ {{ github.repoFor(pr.repoGithubId)?.name }} · {{ pr.headRef }} → {{ pr.baseRef }}
513
+ </div>
514
+ </div>
515
+ <div class="flex items-center gap-2">
516
+ <UBadge
517
+ :color="pr.merged ? 'primary' : pr.state === 'open' ? 'success' : 'neutral'"
518
+ variant="subtle"
519
+ size="sm"
520
+ >
521
+ {{ pr.merged ? 'merged' : pr.state }}
522
+ </UBadge>
523
+ <UButton
524
+ v-if="pr.state === 'open' && !pr.merged"
525
+ size="xs"
526
+ color="neutral"
527
+ variant="ghost"
528
+ icon="i-lucide-git-merge"
529
+ :loading="merging === pr.number"
530
+ @click="merge(pr)"
531
+ />
532
+ <ULink
533
+ :to="github.pullUrl(pr) ?? '#'"
534
+ target="_blank"
535
+ class="text-[11px] text-indigo-400 hover:underline"
536
+ >
537
+ Open
538
+ </ULink>
539
+ </div>
540
+ </div>
541
+ </section>
542
+
543
+ <!-- issues -->
544
+ <section v-else class="space-y-2">
545
+ <p v-if="!github.issues.length" class="py-4 text-sm text-slate-400">
546
+ No issues synced.
547
+ </p>
548
+ <div
549
+ v-for="issue in github.issues"
550
+ :key="`${issue.repoGithubId}-${issue.number}`"
551
+ class="flex items-center justify-between gap-2 rounded-md border border-slate-800 bg-slate-900/60 px-3 py-2"
552
+ >
553
+ <div class="min-w-0">
554
+ <div class="truncate text-sm text-slate-200">
555
+ <span class="text-slate-500">#{{ issue.number }}</span> {{ issue.title }}
556
+ </div>
557
+ <div class="truncate text-[11px] text-slate-500">
558
+ {{ github.repoFor(issue.repoGithubId)?.name }}
559
+ <span v-if="issue.labels.length">· {{ issue.labels.join(', ') }}</span>
560
+ </div>
561
+ </div>
562
+ <div class="flex items-center gap-2">
563
+ <UBadge
564
+ :color="issue.state === 'open' ? 'success' : 'neutral'"
565
+ variant="subtle"
566
+ size="sm"
567
+ >
568
+ {{ issue.state }}
569
+ </UBadge>
570
+ <ULink
571
+ :to="github.issueUrl(issue) ?? '#'"
572
+ target="_blank"
573
+ class="text-[11px] text-indigo-400 hover:underline"
574
+ >
575
+ Open
576
+ </ULink>
577
+ </div>
578
+ </div>
579
+ </section>
580
+ </template>
581
+ </div>
582
+ </template>
583
+ </UModal>
584
+ </template>