@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.
- package/LICENSE +21 -0
- package/README.md +88 -0
- package/app/app.config.ts +8 -0
- package/app/app.vue +11 -0
- package/app/assets/css/main.css +100 -0
- package/app/components/auth/AuthGate.vue +24 -0
- package/app/components/auth/LoginScreen.vue +18 -0
- package/app/components/auth/UserMenu.vue +39 -0
- package/app/components/board/AgentFailureCard.vue +97 -0
- package/app/components/board/AgentStopButton.vue +61 -0
- package/app/components/board/BoardCanvas.vue +146 -0
- package/app/components/board/TaskDependencyEdges.vue +132 -0
- package/app/components/board/nodes/AgentChip.vue +59 -0
- package/app/components/board/nodes/BlockNode.vue +347 -0
- package/app/components/board/nodes/DecisionBadge.vue +21 -0
- package/app/components/board/nodes/DraggableTask.vue +69 -0
- package/app/components/board/nodes/ModuleFrame.vue +70 -0
- package/app/components/board/nodes/TaskCard.vue +237 -0
- package/app/components/bootstrap/BootstrapModal.vue +665 -0
- package/app/components/documents/DocumentImportModal.vue +161 -0
- package/app/components/documents/DocumentSourceConnectModal.vue +127 -0
- package/app/components/documents/SpawnPreviewModal.vue +161 -0
- package/app/components/documents/TaskContextDocs.vue +83 -0
- package/app/components/focus/BlockFocusView.vue +161 -0
- package/app/components/fragments/FragmentLibraryPanel.vue +340 -0
- package/app/components/github/GitHubConnect.vue +183 -0
- package/app/components/github/GitHubPanel.vue +584 -0
- package/app/components/layout/BoardSwitcher.vue +202 -0
- package/app/components/layout/BoardToolbar.vue +109 -0
- package/app/components/layout/SideBar.vue +193 -0
- package/app/components/layout/SpendWarningBanner.vue +107 -0
- package/app/components/palettes/AgentPalette.vue +33 -0
- package/app/components/palettes/BlockPalette.vue +41 -0
- package/app/components/palettes/PipelinePalette.vue +74 -0
- package/app/components/panels/DecisionModal.vue +71 -0
- package/app/components/panels/InspectorPanel.vue +296 -0
- package/app/components/panels/inspector/ContainerSummary.vue +74 -0
- package/app/components/panels/inspector/TaskDependencies.vue +70 -0
- package/app/components/panels/inspector/TaskExecution.vue +175 -0
- package/app/components/panels/inspector/TaskModelSettings.vue +128 -0
- package/app/components/panels/inspector/TaskStructure.vue +139 -0
- package/app/components/pipeline/PipelineBuilder.vue +227 -0
- package/app/components/pipeline/PipelineProgress.vue +246 -0
- package/app/components/requirements/RequirementReviewModal.vue +328 -0
- package/app/components/scenarios/FeatureScenarios.vue +162 -0
- package/app/components/scenarios/ScenarioCard.vue +109 -0
- package/app/components/tasks/TaskContextIssues.vue +88 -0
- package/app/components/tasks/TaskImportModal.vue +140 -0
- package/app/components/tasks/TaskSourceConnectModal.vue +122 -0
- package/app/composables/useApi.ts +535 -0
- package/app/composables/useBlockDrag.ts +75 -0
- package/app/composables/useBlockQueries.ts +136 -0
- package/app/composables/useBoardFlow.ts +11 -0
- package/app/composables/useDepLabels.ts +26 -0
- package/app/composables/useSemanticZoom.ts +16 -0
- package/app/composables/useWorkspaceStream.ts +125 -0
- package/app/docs/architecture.md +31 -0
- package/app/pages/index.vue +80 -0
- package/app/stores/accounts.ts +64 -0
- package/app/stores/agentRuns.ts +117 -0
- package/app/stores/agents.ts +40 -0
- package/app/stores/auth.ts +97 -0
- package/app/stores/board.spec.ts +197 -0
- package/app/stores/board.ts +147 -0
- package/app/stores/bootstrap.ts +97 -0
- package/app/stores/documents.ts +165 -0
- package/app/stores/execution.ts +115 -0
- package/app/stores/fragmentLibrary.ts +147 -0
- package/app/stores/fragments.ts +40 -0
- package/app/stores/github.ts +291 -0
- package/app/stores/models.ts +48 -0
- package/app/stores/pipelines.ts +77 -0
- package/app/stores/requirements.ts +133 -0
- package/app/stores/scenarios.spec.ts +82 -0
- package/app/stores/scenarios.ts +196 -0
- package/app/stores/tasks.spec.ts +71 -0
- package/app/stores/tasks.ts +149 -0
- package/app/stores/ui.ts +204 -0
- package/app/stores/workspace.ts +201 -0
- package/app/types/accounts.ts +38 -0
- package/app/types/bootstrap.ts +83 -0
- package/app/types/documents.ts +92 -0
- package/app/types/domain.ts +216 -0
- package/app/types/execution.ts +110 -0
- package/app/types/fragments.ts +72 -0
- package/app/types/github.ts +153 -0
- package/app/types/models.ts +48 -0
- package/app/types/requirements.ts +38 -0
- package/app/types/scenarios.ts +36 -0
- package/app/types/tasks.ts +67 -0
- package/app/utils/catalog.spec.ts +82 -0
- package/app/utils/catalog.ts +185 -0
- package/app/utils/dnd.ts +29 -0
- package/nuxt.config.ts +43 -0
- 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>
|