@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,340 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Prompt-fragment library manager (ADR 0006): curate this board's best-practice
|
|
3
|
+
// fragments, link repos of Markdown guidelines (with a "changes available" badge
|
|
4
|
+
// + resync), and review the merged catalog (built-in ∪ account ∪ workspace) an
|
|
5
|
+
// agent is selected from per run. Workspace-tier focused; the resolved view shows
|
|
6
|
+
// every tier so the inheritance is visible.
|
|
7
|
+
import type { ResolvedFragment } from '~/types/domain'
|
|
8
|
+
|
|
9
|
+
const ui = useUiStore()
|
|
10
|
+
const library = useFragmentLibraryStore()
|
|
11
|
+
const toast = useToast()
|
|
12
|
+
|
|
13
|
+
const open = computed({
|
|
14
|
+
get: () => ui.fragmentLibraryOpen,
|
|
15
|
+
set: (v: boolean) => {
|
|
16
|
+
if (!v) ui.closeFragmentLibrary()
|
|
17
|
+
},
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
watch(open, (isOpen) => {
|
|
21
|
+
if (isOpen) void library.probe()
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
type Tab = 'catalog' | 'authored' | 'sources'
|
|
25
|
+
const tab = ref<Tab>('catalog')
|
|
26
|
+
|
|
27
|
+
const tierLabel: Record<ResolvedFragment['tier'], string> = {
|
|
28
|
+
builtin: 'Built-in',
|
|
29
|
+
account: 'Account',
|
|
30
|
+
workspace: 'This board',
|
|
31
|
+
}
|
|
32
|
+
// `as const` keeps the literal color names (assignable to UBadge's `color`
|
|
33
|
+
// union) instead of widening to `string`; `satisfies` still checks the shape.
|
|
34
|
+
const tierColor = {
|
|
35
|
+
builtin: 'neutral',
|
|
36
|
+
account: 'info',
|
|
37
|
+
workspace: 'primary',
|
|
38
|
+
} as const satisfies Record<ResolvedFragment['tier'], string>
|
|
39
|
+
|
|
40
|
+
function notifyError(title: string, e: unknown) {
|
|
41
|
+
toast.add({
|
|
42
|
+
title,
|
|
43
|
+
description: e instanceof Error ? e.message : String(e),
|
|
44
|
+
icon: 'i-lucide-triangle-alert',
|
|
45
|
+
color: 'error',
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ---- create a hand-authored fragment --------------------------------------
|
|
50
|
+
const draft = ref({ title: '', summary: '', body: '', tags: '' })
|
|
51
|
+
const draftValid = computed(
|
|
52
|
+
() => draft.value.title.trim() && draft.value.summary.trim() && draft.value.body.trim(),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
async function createFragment() {
|
|
56
|
+
if (!draftValid.value) return
|
|
57
|
+
try {
|
|
58
|
+
await library.create({
|
|
59
|
+
title: draft.value.title.trim(),
|
|
60
|
+
summary: draft.value.summary.trim(),
|
|
61
|
+
body: draft.value.body.trim(),
|
|
62
|
+
tags: draft.value.tags
|
|
63
|
+
.split(',')
|
|
64
|
+
.map((t) => t.trim())
|
|
65
|
+
.filter(Boolean),
|
|
66
|
+
})
|
|
67
|
+
draft.value = { title: '', summary: '', body: '', tags: '' }
|
|
68
|
+
toast.add({ title: 'Fragment added', icon: 'i-lucide-check' })
|
|
69
|
+
} catch (e) {
|
|
70
|
+
notifyError('Could not add fragment', e)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function removeFragment(id: string) {
|
|
75
|
+
try {
|
|
76
|
+
await library.remove(id)
|
|
77
|
+
toast.add({ title: 'Fragment removed', icon: 'i-lucide-trash-2' })
|
|
78
|
+
} catch (e) {
|
|
79
|
+
notifyError('Could not remove fragment', e)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ---- repo sources ----------------------------------------------------------
|
|
84
|
+
const sourceDraft = ref({ repoOwner: '', repoName: '', dirPath: '', gitRef: '' })
|
|
85
|
+
const sourceValid = computed(
|
|
86
|
+
() => sourceDraft.value.repoOwner.trim() && sourceDraft.value.repoName.trim(),
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
async function linkSource() {
|
|
90
|
+
if (!sourceValid.value) return
|
|
91
|
+
try {
|
|
92
|
+
const source = await library.linkSource({
|
|
93
|
+
repoOwner: sourceDraft.value.repoOwner.trim(),
|
|
94
|
+
repoName: sourceDraft.value.repoName.trim(),
|
|
95
|
+
dirPath: sourceDraft.value.dirPath.trim() || undefined,
|
|
96
|
+
gitRef: sourceDraft.value.gitRef.trim() || undefined,
|
|
97
|
+
})
|
|
98
|
+
sourceDraft.value = { repoOwner: '', repoName: '', dirPath: '', gitRef: '' }
|
|
99
|
+
await library.syncSource(source.id)
|
|
100
|
+
toast.add({ title: 'Source linked & synced', icon: 'i-lucide-git-branch' })
|
|
101
|
+
} catch (e) {
|
|
102
|
+
notifyError('Could not link source', e)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function syncSource(id: string) {
|
|
107
|
+
try {
|
|
108
|
+
const result = await library.syncSource(id)
|
|
109
|
+
toast.add({
|
|
110
|
+
title: `Synced: ${result.upserted} updated, ${result.tombstoned} removed`,
|
|
111
|
+
icon: 'i-lucide-refresh-cw',
|
|
112
|
+
color: 'info',
|
|
113
|
+
})
|
|
114
|
+
} catch (e) {
|
|
115
|
+
notifyError('Could not sync source', e)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function checkSource(id: string) {
|
|
120
|
+
try {
|
|
121
|
+
const status = await library.checkSource(id)
|
|
122
|
+
toast.add({
|
|
123
|
+
title: status.changed ? `${status.changedCount} change(s) available` : 'Up to date',
|
|
124
|
+
icon: status.changed ? 'i-lucide-bell-dot' : 'i-lucide-check',
|
|
125
|
+
})
|
|
126
|
+
} catch (e) {
|
|
127
|
+
notifyError('Could not check source', e)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function unlinkSource(id: string) {
|
|
132
|
+
try {
|
|
133
|
+
await library.unlinkSource(id)
|
|
134
|
+
toast.add({ title: 'Source unlinked', icon: 'i-lucide-unplug' })
|
|
135
|
+
} catch (e) {
|
|
136
|
+
notifyError('Could not unlink source', e)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
</script>
|
|
140
|
+
|
|
141
|
+
<template>
|
|
142
|
+
<UModal v-model:open="open" title="Prompt-fragment library" :ui="{ content: 'max-w-3xl' }">
|
|
143
|
+
<template #body>
|
|
144
|
+
<div class="flex flex-col gap-4">
|
|
145
|
+
<p class="text-sm text-slate-400">
|
|
146
|
+
Curate the best-practice guidelines agents follow on this board. Fragments are merged from
|
|
147
|
+
the built-in catalog, your account, and this board — later tiers override earlier ones —
|
|
148
|
+
then the relevant ones are selected for each agent run.
|
|
149
|
+
</p>
|
|
150
|
+
|
|
151
|
+
<div class="flex gap-2">
|
|
152
|
+
<UButton
|
|
153
|
+
v-for="t in ['catalog', 'authored', 'sources'] as Tab[]"
|
|
154
|
+
:key="t"
|
|
155
|
+
:color="tab === t ? 'primary' : 'neutral'"
|
|
156
|
+
:variant="tab === t ? 'solid' : 'ghost'"
|
|
157
|
+
size="sm"
|
|
158
|
+
@click="tab = t"
|
|
159
|
+
>
|
|
160
|
+
{{
|
|
161
|
+
t === 'catalog'
|
|
162
|
+
? 'Resolved catalog'
|
|
163
|
+
: t === 'authored'
|
|
164
|
+
? 'This board'
|
|
165
|
+
: 'Repo sources'
|
|
166
|
+
}}
|
|
167
|
+
</UButton>
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
<!-- Resolved (merged) catalog -->
|
|
171
|
+
<div v-if="tab === 'catalog'" class="flex flex-col gap-2">
|
|
172
|
+
<p class="text-xs text-slate-500">
|
|
173
|
+
{{ library.resolved.length }} fragment(s) resolved ·
|
|
174
|
+
{{ library.builtinCount }} built-in.
|
|
175
|
+
</p>
|
|
176
|
+
<div
|
|
177
|
+
v-for="f in library.resolved"
|
|
178
|
+
:key="f.id"
|
|
179
|
+
class="rounded-md border border-slate-800 bg-slate-900/60 p-3"
|
|
180
|
+
>
|
|
181
|
+
<div class="flex items-center gap-2">
|
|
182
|
+
<span class="font-medium text-slate-100">{{ f.title }}</span>
|
|
183
|
+
<UBadge size="xs" :color="tierColor[f.tier]" variant="subtle">
|
|
184
|
+
{{ tierLabel[f.tier] }}
|
|
185
|
+
</UBadge>
|
|
186
|
+
<span class="ml-auto font-mono text-[11px] text-slate-500">{{ f.id }}</span>
|
|
187
|
+
</div>
|
|
188
|
+
<p class="mt-1 text-sm text-slate-400">{{ f.summary }}</p>
|
|
189
|
+
<div v-if="f.tags?.length" class="mt-1 flex flex-wrap gap-1">
|
|
190
|
+
<UBadge v-for="tag in f.tags" :key="tag" size="xs" variant="outline" color="neutral">
|
|
191
|
+
{{ tag }}
|
|
192
|
+
</UBadge>
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
<!-- Hand-authored (workspace tier) -->
|
|
198
|
+
<div v-else-if="tab === 'authored'" class="flex flex-col gap-3">
|
|
199
|
+
<div
|
|
200
|
+
v-for="f in library.fragments"
|
|
201
|
+
:key="f.id"
|
|
202
|
+
class="flex items-start gap-2 rounded-md border border-slate-800 bg-slate-900/60 p-3"
|
|
203
|
+
>
|
|
204
|
+
<div class="min-w-0">
|
|
205
|
+
<div class="flex items-center gap-2">
|
|
206
|
+
<span class="font-medium text-slate-100">{{ f.title }}</span>
|
|
207
|
+
<UBadge v-if="f.source" size="xs" color="info" variant="subtle">from repo</UBadge>
|
|
208
|
+
</div>
|
|
209
|
+
<p class="text-sm text-slate-400">{{ f.summary }}</p>
|
|
210
|
+
</div>
|
|
211
|
+
<UButton
|
|
212
|
+
icon="i-lucide-trash-2"
|
|
213
|
+
size="xs"
|
|
214
|
+
color="error"
|
|
215
|
+
variant="ghost"
|
|
216
|
+
class="ml-auto"
|
|
217
|
+
@click="removeFragment(f.id)"
|
|
218
|
+
/>
|
|
219
|
+
</div>
|
|
220
|
+
<p v-if="!library.fragments.length" class="text-sm text-slate-500">
|
|
221
|
+
No board-specific fragments yet. Add one below, or override a built-in by using its id.
|
|
222
|
+
</p>
|
|
223
|
+
|
|
224
|
+
<div class="rounded-md border border-slate-800 p-3">
|
|
225
|
+
<p class="mb-2 text-sm font-medium">Add a fragment</p>
|
|
226
|
+
<div class="flex flex-col gap-2">
|
|
227
|
+
<UInput v-model="draft.title" placeholder="Title" />
|
|
228
|
+
<UInput
|
|
229
|
+
v-model="draft.summary"
|
|
230
|
+
placeholder="One-line summary (used by the selector)"
|
|
231
|
+
/>
|
|
232
|
+
<UTextarea
|
|
233
|
+
v-model="draft.body"
|
|
234
|
+
placeholder="Guidance body (injected into the prompt)"
|
|
235
|
+
:rows="4"
|
|
236
|
+
/>
|
|
237
|
+
<UInput v-model="draft.tags" placeholder="Tags, comma-separated (e.g. backend, db)" />
|
|
238
|
+
<UButton
|
|
239
|
+
icon="i-lucide-plus"
|
|
240
|
+
size="sm"
|
|
241
|
+
:disabled="!draftValid"
|
|
242
|
+
:loading="library.loading"
|
|
243
|
+
class="self-start"
|
|
244
|
+
@click="createFragment"
|
|
245
|
+
>
|
|
246
|
+
Add fragment
|
|
247
|
+
</UButton>
|
|
248
|
+
</div>
|
|
249
|
+
</div>
|
|
250
|
+
</div>
|
|
251
|
+
|
|
252
|
+
<!-- Repo sources -->
|
|
253
|
+
<div v-else class="flex flex-col gap-3">
|
|
254
|
+
<div
|
|
255
|
+
v-for="s in library.sources"
|
|
256
|
+
:key="s.id"
|
|
257
|
+
class="flex items-center gap-2 rounded-md border border-slate-800 bg-slate-900/60 p-3"
|
|
258
|
+
>
|
|
259
|
+
<UIcon name="i-lucide-git-branch" class="h-4 w-4 text-slate-400" />
|
|
260
|
+
<div class="min-w-0">
|
|
261
|
+
<span class="font-mono text-sm text-slate-100">
|
|
262
|
+
{{ s.repoOwner }}/{{ s.repoName
|
|
263
|
+
}}<span class="text-slate-500">/{{ s.dirPath || '' }}</span>
|
|
264
|
+
</span>
|
|
265
|
+
<p class="text-xs text-slate-500">
|
|
266
|
+
{{ s.lastSyncedAt ? 'synced' : 'never synced' }} · ref {{ s.gitRef }}
|
|
267
|
+
</p>
|
|
268
|
+
</div>
|
|
269
|
+
<UBadge
|
|
270
|
+
v-if="library.sourceChanges[s.id]"
|
|
271
|
+
size="xs"
|
|
272
|
+
color="warning"
|
|
273
|
+
variant="subtle"
|
|
274
|
+
class="ml-auto"
|
|
275
|
+
>
|
|
276
|
+
{{ library.sourceChanges[s.id] }} change(s)
|
|
277
|
+
</UBadge>
|
|
278
|
+
<div class="ml-auto flex gap-1">
|
|
279
|
+
<UButton
|
|
280
|
+
icon="i-lucide-search-check"
|
|
281
|
+
size="xs"
|
|
282
|
+
variant="ghost"
|
|
283
|
+
@click="checkSource(s.id)"
|
|
284
|
+
/>
|
|
285
|
+
<UButton
|
|
286
|
+
icon="i-lucide-refresh-cw"
|
|
287
|
+
size="xs"
|
|
288
|
+
variant="ghost"
|
|
289
|
+
:loading="library.loading"
|
|
290
|
+
@click="syncSource(s.id)"
|
|
291
|
+
/>
|
|
292
|
+
<UButton
|
|
293
|
+
icon="i-lucide-unplug"
|
|
294
|
+
size="xs"
|
|
295
|
+
color="error"
|
|
296
|
+
variant="ghost"
|
|
297
|
+
@click="unlinkSource(s.id)"
|
|
298
|
+
/>
|
|
299
|
+
</div>
|
|
300
|
+
</div>
|
|
301
|
+
<p v-if="!library.sources.length" class="text-sm text-slate-500">
|
|
302
|
+
No linked guideline repos. Link one below to import its Markdown files as fragments.
|
|
303
|
+
</p>
|
|
304
|
+
|
|
305
|
+
<div class="rounded-md border border-slate-800 p-3">
|
|
306
|
+
<p class="mb-2 text-sm font-medium">Link a guideline repo</p>
|
|
307
|
+
<div class="flex flex-col gap-2">
|
|
308
|
+
<div class="flex gap-2">
|
|
309
|
+
<UInput v-model="sourceDraft.repoOwner" placeholder="owner" class="flex-1" />
|
|
310
|
+
<UInput v-model="sourceDraft.repoName" placeholder="repo" class="flex-1" />
|
|
311
|
+
</div>
|
|
312
|
+
<div class="flex gap-2">
|
|
313
|
+
<UInput
|
|
314
|
+
v-model="sourceDraft.dirPath"
|
|
315
|
+
placeholder="dir path (e.g. guidelines)"
|
|
316
|
+
class="flex-1"
|
|
317
|
+
/>
|
|
318
|
+
<UInput
|
|
319
|
+
v-model="sourceDraft.gitRef"
|
|
320
|
+
placeholder="ref (default HEAD)"
|
|
321
|
+
class="flex-1"
|
|
322
|
+
/>
|
|
323
|
+
</div>
|
|
324
|
+
<UButton
|
|
325
|
+
icon="i-lucide-link"
|
|
326
|
+
size="sm"
|
|
327
|
+
:disabled="!sourceValid"
|
|
328
|
+
:loading="library.loading"
|
|
329
|
+
class="self-start"
|
|
330
|
+
@click="linkSource"
|
|
331
|
+
>
|
|
332
|
+
Link & sync
|
|
333
|
+
</UButton>
|
|
334
|
+
</div>
|
|
335
|
+
</div>
|
|
336
|
+
</div>
|
|
337
|
+
</div>
|
|
338
|
+
</template>
|
|
339
|
+
</UModal>
|
|
340
|
+
</template>
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Discover-and-link surface for the workspace's GitHub App installation. Lists
|
|
3
|
+
// the installations the App is already on (via the app JWT) so the user can bind
|
|
4
|
+
// one with a single click — no installation-id typing — falling back to the
|
|
5
|
+
// install redirect and a manual-id entry. Self-loads its list on mount; on a
|
|
6
|
+
// successful connect the github store flips `connected`, which the host surfaces
|
|
7
|
+
// react to. Shared by the GitHub panel and the bootstrap modal so the connect
|
|
8
|
+
// flow lives in one place.
|
|
9
|
+
const github = useGitHubStore()
|
|
10
|
+
const toast = useToast()
|
|
11
|
+
|
|
12
|
+
const installing = ref(false)
|
|
13
|
+
const installationId = ref('')
|
|
14
|
+
const connecting = ref(false)
|
|
15
|
+
// Track which installation row is mid-connect so only its button spins.
|
|
16
|
+
const connectingId = ref<number | null>(null)
|
|
17
|
+
|
|
18
|
+
onMounted(() => {
|
|
19
|
+
void refreshInstallations()
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
function notifyError(title: string, e: unknown) {
|
|
23
|
+
toast.add({
|
|
24
|
+
title,
|
|
25
|
+
description: e instanceof Error ? e.message : String(e),
|
|
26
|
+
icon: 'i-lucide-triangle-alert',
|
|
27
|
+
color: 'error',
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function refreshInstallations() {
|
|
32
|
+
try {
|
|
33
|
+
await github.loadInstallations()
|
|
34
|
+
} catch (e) {
|
|
35
|
+
// A 503 (integration off) is handled by the host; surface anything else.
|
|
36
|
+
notifyError('Could not list GitHub installations', e)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function install() {
|
|
41
|
+
installing.value = true
|
|
42
|
+
try {
|
|
43
|
+
window.location.href = await github.getInstallUrl()
|
|
44
|
+
} catch (e) {
|
|
45
|
+
notifyError('Could not start the GitHub App install', e)
|
|
46
|
+
installing.value = false
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function connect(id: number, onDone?: () => void) {
|
|
51
|
+
connecting.value = true
|
|
52
|
+
connectingId.value = id
|
|
53
|
+
try {
|
|
54
|
+
await github.connect(id)
|
|
55
|
+
onDone?.()
|
|
56
|
+
toast.add({ title: 'GitHub connected', icon: 'i-lucide-check', color: 'success' })
|
|
57
|
+
} catch (e) {
|
|
58
|
+
notifyError('Could not connect', e)
|
|
59
|
+
} finally {
|
|
60
|
+
connecting.value = false
|
|
61
|
+
connectingId.value = null
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function connectManually() {
|
|
66
|
+
const id = Number(installationId.value.trim())
|
|
67
|
+
if (!Number.isInteger(id) || id <= 0) return
|
|
68
|
+
await connect(id, () => {
|
|
69
|
+
installationId.value = ''
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
</script>
|
|
73
|
+
|
|
74
|
+
<template>
|
|
75
|
+
<div class="space-y-3">
|
|
76
|
+
<!-- discovered installations: pick one the App is already on -->
|
|
77
|
+
<section class="space-y-2">
|
|
78
|
+
<div class="flex items-center justify-between">
|
|
79
|
+
<span class="text-xs font-medium uppercase tracking-wide text-slate-500">
|
|
80
|
+
Your installations
|
|
81
|
+
</span>
|
|
82
|
+
<UButton
|
|
83
|
+
size="xs"
|
|
84
|
+
color="neutral"
|
|
85
|
+
variant="ghost"
|
|
86
|
+
icon="i-lucide-refresh-cw"
|
|
87
|
+
:loading="github.loadingInstallations"
|
|
88
|
+
@click="refreshInstallations"
|
|
89
|
+
>
|
|
90
|
+
Refresh
|
|
91
|
+
</UButton>
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
<div
|
|
95
|
+
v-if="github.loadingInstallations && !github.installations.length"
|
|
96
|
+
class="flex items-center gap-2 py-3 text-sm text-slate-400"
|
|
97
|
+
>
|
|
98
|
+
<UIcon name="i-lucide-loader" class="h-4 w-4 animate-spin" /> Looking for installations…
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<p
|
|
102
|
+
v-else-if="!github.installations.length"
|
|
103
|
+
class="rounded-md border border-dashed border-slate-800 px-3 py-3 text-sm text-slate-400"
|
|
104
|
+
>
|
|
105
|
+
No installations found for the App yet. Install it below, or connect by ID.
|
|
106
|
+
</p>
|
|
107
|
+
|
|
108
|
+
<div
|
|
109
|
+
v-for="inst in github.installations"
|
|
110
|
+
:key="inst.installationId"
|
|
111
|
+
class="flex items-center justify-between gap-2 rounded-md border border-slate-800 bg-slate-900/60 px-3 py-2"
|
|
112
|
+
>
|
|
113
|
+
<div class="flex min-w-0 items-center gap-2">
|
|
114
|
+
<UAvatar
|
|
115
|
+
v-if="inst.accountAvatarUrl"
|
|
116
|
+
:src="inst.accountAvatarUrl"
|
|
117
|
+
size="2xs"
|
|
118
|
+
:alt="inst.accountLogin"
|
|
119
|
+
/>
|
|
120
|
+
<UIcon v-else name="i-lucide-github" class="h-4 w-4 text-slate-400" />
|
|
121
|
+
<div class="min-w-0">
|
|
122
|
+
<div class="truncate text-sm text-slate-200">{{ inst.accountLogin }}</div>
|
|
123
|
+
<div class="text-[11px] text-slate-500">
|
|
124
|
+
{{ inst.targetType }} · installation {{ inst.installationId }}
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
<UBadge
|
|
129
|
+
v-if="inst.connected === 'this'"
|
|
130
|
+
color="success"
|
|
131
|
+
variant="subtle"
|
|
132
|
+
size="sm"
|
|
133
|
+
title="Already linked to this workspace"
|
|
134
|
+
>
|
|
135
|
+
linked
|
|
136
|
+
</UBadge>
|
|
137
|
+
<UBadge
|
|
138
|
+
v-else-if="inst.connected === 'other'"
|
|
139
|
+
color="neutral"
|
|
140
|
+
variant="subtle"
|
|
141
|
+
size="sm"
|
|
142
|
+
title="Already connected to another workspace"
|
|
143
|
+
>
|
|
144
|
+
in use
|
|
145
|
+
</UBadge>
|
|
146
|
+
<UButton
|
|
147
|
+
v-else
|
|
148
|
+
size="xs"
|
|
149
|
+
color="primary"
|
|
150
|
+
variant="subtle"
|
|
151
|
+
icon="i-lucide-plug"
|
|
152
|
+
:loading="connectingId === inst.installationId"
|
|
153
|
+
:disabled="connecting"
|
|
154
|
+
@click="connect(inst.installationId)"
|
|
155
|
+
>
|
|
156
|
+
Connect
|
|
157
|
+
</UButton>
|
|
158
|
+
</div>
|
|
159
|
+
</section>
|
|
160
|
+
|
|
161
|
+
<USeparator label="or" />
|
|
162
|
+
<UButton color="primary" icon="i-lucide-github" :loading="installing" @click="install">
|
|
163
|
+
Install GitHub App
|
|
164
|
+
</UButton>
|
|
165
|
+
|
|
166
|
+
<USeparator label="or connect by ID" />
|
|
167
|
+
<div class="flex items-end gap-2">
|
|
168
|
+
<UFormField label="Installation ID" class="flex-1">
|
|
169
|
+
<UInput v-model="installationId" type="number" placeholder="12345678" class="w-full" />
|
|
170
|
+
</UFormField>
|
|
171
|
+
<UButton
|
|
172
|
+
color="neutral"
|
|
173
|
+
variant="subtle"
|
|
174
|
+
icon="i-lucide-plug"
|
|
175
|
+
:loading="connecting && connectingId === null"
|
|
176
|
+
:disabled="!installationId.trim()"
|
|
177
|
+
@click="connectManually"
|
|
178
|
+
>
|
|
179
|
+
Connect
|
|
180
|
+
</UButton>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
</template>
|