@cat-factory/app 0.33.0 → 0.35.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/app/components/fragments/FragmentLibraryManager.vue +511 -0
- package/app/components/fragments/FragmentLibraryPanel.vue +12 -458
- package/app/components/layout/AccountFragmentSettings.vue +37 -0
- package/app/components/layout/BoardSwitcher.vue +4 -3
- package/app/components/layout/CommandBar.vue +9 -0
- package/app/components/layout/IntegrationsHub.vue +13 -0
- package/app/components/settings/AccountSettingsPanel.vue +40 -7
- package/app/components/settings/LocalModeSettingsPanel.vue +159 -0
- package/app/components/settings/ServiceFragmentDefaultsPanel.vue +21 -0
- package/app/composables/api/fragments.ts +12 -3
- package/app/composables/api/localSettings.ts +17 -0
- package/app/composables/useApi.ts +2 -0
- package/app/pages/index.vue +2 -0
- package/app/stores/fragmentLibrary.ts +80 -31
- package/app/stores/localSettings.ts +48 -0
- package/app/stores/ui.ts +26 -3
- package/app/types/localSettings.ts +31 -0
- package/package.json +1 -1
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Prompt-fragment library manager (ADR 0006), reused at two scopes: a board
|
|
3
|
+
// (`workspace`) and an `account`. Curate hand-authored fragments, link external
|
|
4
|
+
// documents as living fragments, link repos of Markdown guidelines (with a
|
|
5
|
+
// "changes available" badge + resync), and — at the workspace scope only — review
|
|
6
|
+
// the merged catalog (built-in ∪ account ∪ workspace) an agent is selected from per
|
|
7
|
+
// run. The account scope has no resolved/merged catalog and fetches document
|
|
8
|
+
// fragments through `viaWorkspaceId` (document-source credentials are per-workspace).
|
|
9
|
+
import { computed, ref, watch } from 'vue'
|
|
10
|
+
import type { DocumentSourceKind, FragmentOwnerKind, ResolvedFragment } from '~/types/domain'
|
|
11
|
+
import { useFragmentLibrary, useFragmentLibraryStore } from '~/stores/fragmentLibrary'
|
|
12
|
+
|
|
13
|
+
const props = withDefaults(
|
|
14
|
+
defineProps<{
|
|
15
|
+
kind: FragmentOwnerKind
|
|
16
|
+
ownerId: string
|
|
17
|
+
/** Account scope only: the workspace whose document-source connection to fetch through. */
|
|
18
|
+
viaWorkspaceId?: string
|
|
19
|
+
/** Whether to show the resolved/merged catalog tab (workspace scope only). */
|
|
20
|
+
showCatalog?: boolean
|
|
21
|
+
}>(),
|
|
22
|
+
{ showCatalog: false },
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
// The workspace scope follows the active board (singleton, shared with the navbar);
|
|
26
|
+
// the account scope uses an owner-keyed store so each account is isolated.
|
|
27
|
+
const library =
|
|
28
|
+
props.kind === 'workspace'
|
|
29
|
+
? useFragmentLibraryStore()
|
|
30
|
+
: useFragmentLibrary(props.kind, props.ownerId)
|
|
31
|
+
const documents = useDocumentsStore()
|
|
32
|
+
const toast = useToast()
|
|
33
|
+
|
|
34
|
+
const isWorkspace = props.kind === 'workspace'
|
|
35
|
+
/** Linking a document at the account scope needs a workspace connection to fetch through. */
|
|
36
|
+
const docLinkDisabled = computed(() => props.kind === 'account' && !props.viaWorkspaceId)
|
|
37
|
+
|
|
38
|
+
watch(
|
|
39
|
+
() => props.viaWorkspaceId,
|
|
40
|
+
(id) => {
|
|
41
|
+
library.viaWorkspaceId = id
|
|
42
|
+
},
|
|
43
|
+
{ immediate: true },
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
watch(
|
|
47
|
+
() => props.ownerId,
|
|
48
|
+
() => {
|
|
49
|
+
void library.probe()
|
|
50
|
+
void documents.probe()
|
|
51
|
+
},
|
|
52
|
+
{ immediate: true },
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
type Tab = 'catalog' | 'authored' | 'documents' | 'sources'
|
|
56
|
+
const tabs = computed<Tab[]>(() =>
|
|
57
|
+
props.showCatalog
|
|
58
|
+
? ['catalog', 'authored', 'documents', 'sources']
|
|
59
|
+
: ['authored', 'documents', 'sources'],
|
|
60
|
+
)
|
|
61
|
+
const tab = ref<Tab>(props.showCatalog ? 'catalog' : 'authored')
|
|
62
|
+
|
|
63
|
+
const ownerLabel = isWorkspace ? 'This board' : 'This account'
|
|
64
|
+
|
|
65
|
+
const tierLabel: Record<ResolvedFragment['tier'], string> = {
|
|
66
|
+
builtin: 'Built-in',
|
|
67
|
+
account: 'Account',
|
|
68
|
+
workspace: 'This board',
|
|
69
|
+
}
|
|
70
|
+
// `as const` keeps the literal color names (assignable to UBadge's `color`
|
|
71
|
+
// union) instead of widening to `string`; `satisfies` still checks the shape.
|
|
72
|
+
const tierColor = {
|
|
73
|
+
builtin: 'neutral',
|
|
74
|
+
account: 'info',
|
|
75
|
+
workspace: 'primary',
|
|
76
|
+
} as const satisfies Record<ResolvedFragment['tier'], string>
|
|
77
|
+
|
|
78
|
+
function tabLabel(t: Tab): string {
|
|
79
|
+
if (t === 'catalog') return 'Resolved catalog'
|
|
80
|
+
if (t === 'authored') return ownerLabel
|
|
81
|
+
if (t === 'documents') return 'Documents'
|
|
82
|
+
return 'Repo sources'
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function notifyError(title: string, e: unknown) {
|
|
86
|
+
toast.add({
|
|
87
|
+
title,
|
|
88
|
+
description: e instanceof Error ? e.message : String(e),
|
|
89
|
+
icon: 'i-lucide-triangle-alert',
|
|
90
|
+
color: 'error',
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ---- create a hand-authored fragment --------------------------------------
|
|
95
|
+
const draft = ref({ title: '', summary: '', body: '', tags: '' })
|
|
96
|
+
const draftValid = computed(
|
|
97
|
+
() => draft.value.title.trim() && draft.value.summary.trim() && draft.value.body.trim(),
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
async function createFragment() {
|
|
101
|
+
if (!draftValid.value) return
|
|
102
|
+
try {
|
|
103
|
+
await library.create({
|
|
104
|
+
title: draft.value.title.trim(),
|
|
105
|
+
summary: draft.value.summary.trim(),
|
|
106
|
+
body: draft.value.body.trim(),
|
|
107
|
+
tags: draft.value.tags
|
|
108
|
+
.split(',')
|
|
109
|
+
.map((t) => t.trim())
|
|
110
|
+
.filter(Boolean),
|
|
111
|
+
})
|
|
112
|
+
draft.value = { title: '', summary: '', body: '', tags: '' }
|
|
113
|
+
toast.add({ title: 'Fragment added', icon: 'i-lucide-check' })
|
|
114
|
+
} catch (e) {
|
|
115
|
+
notifyError('Could not add fragment', e)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function removeFragment(id: string) {
|
|
120
|
+
try {
|
|
121
|
+
await library.remove(id)
|
|
122
|
+
toast.add({ title: 'Fragment removed', icon: 'i-lucide-trash-2' })
|
|
123
|
+
} catch (e) {
|
|
124
|
+
notifyError('Could not remove fragment', e)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ---- document-backed (living) fragments -----------------------------------
|
|
129
|
+
// Link a Confluence/Notion page or GitHub file as a fragment that is re-resolved
|
|
130
|
+
// from the source at run time (a living source of truth, not a frozen snapshot).
|
|
131
|
+
const docDraft = ref({ source: '' as DocumentSourceKind | '', ref: '', tags: '' })
|
|
132
|
+
const docDraftValid = computed(
|
|
133
|
+
() => !docLinkDisabled.value && docDraft.value.source && docDraft.value.ref.trim(),
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
/** This tier's existing document-backed fragments. */
|
|
137
|
+
const documentFragments = computed(() => library.fragments.filter((f) => f.documentRef))
|
|
138
|
+
|
|
139
|
+
async function linkDocumentFragment() {
|
|
140
|
+
if (!docDraftValid.value) return
|
|
141
|
+
try {
|
|
142
|
+
await library.createDocumentFragment({
|
|
143
|
+
source: docDraft.value.source as DocumentSourceKind,
|
|
144
|
+
ref: docDraft.value.ref.trim(),
|
|
145
|
+
tags: docDraft.value.tags
|
|
146
|
+
.split(',')
|
|
147
|
+
.map((t) => t.trim())
|
|
148
|
+
.filter(Boolean),
|
|
149
|
+
})
|
|
150
|
+
docDraft.value = { source: '', ref: '', tags: '' }
|
|
151
|
+
toast.add({ title: 'Document linked as a living fragment', icon: 'i-lucide-link' })
|
|
152
|
+
} catch (e) {
|
|
153
|
+
notifyError('Could not link document', e)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function refreshFragment(id: string) {
|
|
158
|
+
try {
|
|
159
|
+
await library.refreshDocumentFragment(id)
|
|
160
|
+
toast.add({ title: 'Fragment re-resolved from source', icon: 'i-lucide-refresh-cw' })
|
|
161
|
+
} catch (e) {
|
|
162
|
+
notifyError('Could not refresh fragment', e)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ---- repo sources ----------------------------------------------------------
|
|
167
|
+
const sourceDraft = ref({ repoOwner: '', repoName: '', dirPath: '', gitRef: '' })
|
|
168
|
+
const sourceValid = computed(
|
|
169
|
+
() => sourceDraft.value.repoOwner.trim() && sourceDraft.value.repoName.trim(),
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
async function linkSource() {
|
|
173
|
+
if (!sourceValid.value) return
|
|
174
|
+
try {
|
|
175
|
+
const source = await library.linkSource({
|
|
176
|
+
repoOwner: sourceDraft.value.repoOwner.trim(),
|
|
177
|
+
repoName: sourceDraft.value.repoName.trim(),
|
|
178
|
+
dirPath: sourceDraft.value.dirPath.trim() || undefined,
|
|
179
|
+
gitRef: sourceDraft.value.gitRef.trim() || undefined,
|
|
180
|
+
})
|
|
181
|
+
sourceDraft.value = { repoOwner: '', repoName: '', dirPath: '', gitRef: '' }
|
|
182
|
+
await library.syncSource(source.id)
|
|
183
|
+
toast.add({ title: 'Source linked & synced', icon: 'i-lucide-git-branch' })
|
|
184
|
+
} catch (e) {
|
|
185
|
+
notifyError('Could not link source', e)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function syncSource(id: string) {
|
|
190
|
+
try {
|
|
191
|
+
const result = await library.syncSource(id)
|
|
192
|
+
toast.add({
|
|
193
|
+
title: `Synced: ${result.upserted} updated, ${result.tombstoned} removed`,
|
|
194
|
+
icon: 'i-lucide-refresh-cw',
|
|
195
|
+
color: 'info',
|
|
196
|
+
})
|
|
197
|
+
} catch (e) {
|
|
198
|
+
notifyError('Could not sync source', e)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function checkSource(id: string) {
|
|
203
|
+
try {
|
|
204
|
+
const status = await library.checkSource(id)
|
|
205
|
+
toast.add({
|
|
206
|
+
title: status.changed ? `${status.changedCount} change(s) available` : 'Up to date',
|
|
207
|
+
icon: status.changed ? 'i-lucide-bell-dot' : 'i-lucide-check',
|
|
208
|
+
})
|
|
209
|
+
} catch (e) {
|
|
210
|
+
notifyError('Could not check source', e)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function unlinkSource(id: string) {
|
|
215
|
+
try {
|
|
216
|
+
await library.unlinkSource(id)
|
|
217
|
+
toast.add({ title: 'Source unlinked', icon: 'i-lucide-unplug' })
|
|
218
|
+
} catch (e) {
|
|
219
|
+
notifyError('Could not unlink source', e)
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
</script>
|
|
223
|
+
|
|
224
|
+
<template>
|
|
225
|
+
<div class="flex flex-col gap-4">
|
|
226
|
+
<p class="text-sm text-slate-400">
|
|
227
|
+
<template v-if="isWorkspace">
|
|
228
|
+
Curate the best-practice guidelines agents follow on this board. Fragments are merged from
|
|
229
|
+
the built-in catalog, your account, and this board — later tiers override earlier ones —
|
|
230
|
+
then the relevant ones are selected for each agent run.
|
|
231
|
+
</template>
|
|
232
|
+
<template v-else>
|
|
233
|
+
Curate best-practice guidelines shared by every board in this account. Account fragments are
|
|
234
|
+
merged below the built-in catalog and above nothing, and a board can override or add its own
|
|
235
|
+
on top. The relevant ones are selected for each agent run.
|
|
236
|
+
</template>
|
|
237
|
+
</p>
|
|
238
|
+
|
|
239
|
+
<div class="flex gap-2">
|
|
240
|
+
<UButton
|
|
241
|
+
v-for="t in tabs"
|
|
242
|
+
:key="t"
|
|
243
|
+
:color="tab === t ? 'primary' : 'neutral'"
|
|
244
|
+
:variant="tab === t ? 'solid' : 'ghost'"
|
|
245
|
+
size="sm"
|
|
246
|
+
@click="tab = t"
|
|
247
|
+
>
|
|
248
|
+
{{ tabLabel(t) }}
|
|
249
|
+
</UButton>
|
|
250
|
+
</div>
|
|
251
|
+
|
|
252
|
+
<!-- Resolved (merged) catalog — workspace scope only -->
|
|
253
|
+
<div v-if="tab === 'catalog'" class="flex flex-col gap-2">
|
|
254
|
+
<p class="text-xs text-slate-500">
|
|
255
|
+
{{ library.resolved.length }} fragment(s) resolved · {{ library.builtinCount }} built-in.
|
|
256
|
+
</p>
|
|
257
|
+
<div
|
|
258
|
+
v-for="f in library.resolved"
|
|
259
|
+
:key="f.id"
|
|
260
|
+
class="rounded-md border border-slate-800 bg-slate-900/60 p-3"
|
|
261
|
+
>
|
|
262
|
+
<div class="flex items-center gap-2">
|
|
263
|
+
<span class="font-medium text-slate-100">{{ f.title }}</span>
|
|
264
|
+
<UBadge size="xs" :color="tierColor[f.tier]" variant="subtle">
|
|
265
|
+
{{ tierLabel[f.tier] }}
|
|
266
|
+
</UBadge>
|
|
267
|
+
<UBadge
|
|
268
|
+
v-if="f.documentRef"
|
|
269
|
+
size="xs"
|
|
270
|
+
color="success"
|
|
271
|
+
variant="subtle"
|
|
272
|
+
icon="i-lucide-radio"
|
|
273
|
+
>
|
|
274
|
+
Live · {{ f.documentRef.source }}
|
|
275
|
+
</UBadge>
|
|
276
|
+
<span class="ml-auto font-mono text-[11px] text-slate-500">{{ f.id }}</span>
|
|
277
|
+
</div>
|
|
278
|
+
<p class="mt-1 text-sm text-slate-400">{{ f.summary }}</p>
|
|
279
|
+
<div v-if="f.tags?.length" class="mt-1 flex flex-wrap gap-1">
|
|
280
|
+
<UBadge v-for="tag in f.tags" :key="tag" size="xs" variant="outline" color="neutral">
|
|
281
|
+
{{ tag }}
|
|
282
|
+
</UBadge>
|
|
283
|
+
</div>
|
|
284
|
+
</div>
|
|
285
|
+
</div>
|
|
286
|
+
|
|
287
|
+
<!-- Hand-authored (this tier) -->
|
|
288
|
+
<div v-else-if="tab === 'authored'" class="flex flex-col gap-3">
|
|
289
|
+
<div
|
|
290
|
+
v-for="f in library.fragments"
|
|
291
|
+
:key="f.id"
|
|
292
|
+
class="flex items-start gap-2 rounded-md border border-slate-800 bg-slate-900/60 p-3"
|
|
293
|
+
>
|
|
294
|
+
<div class="min-w-0">
|
|
295
|
+
<div class="flex items-center gap-2">
|
|
296
|
+
<span class="font-medium text-slate-100">{{ f.title }}</span>
|
|
297
|
+
<UBadge v-if="f.source" size="xs" color="info" variant="subtle">from repo</UBadge>
|
|
298
|
+
</div>
|
|
299
|
+
<p class="text-sm text-slate-400">{{ f.summary }}</p>
|
|
300
|
+
</div>
|
|
301
|
+
<UButton
|
|
302
|
+
icon="i-lucide-trash-2"
|
|
303
|
+
size="xs"
|
|
304
|
+
color="error"
|
|
305
|
+
variant="ghost"
|
|
306
|
+
class="ml-auto"
|
|
307
|
+
@click="removeFragment(f.id)"
|
|
308
|
+
/>
|
|
309
|
+
</div>
|
|
310
|
+
<p v-if="!library.fragments.length" class="text-sm text-slate-500">
|
|
311
|
+
No {{ isWorkspace ? 'board' : 'account' }}-specific fragments yet. Add one below, or
|
|
312
|
+
override a built-in by using its id.
|
|
313
|
+
</p>
|
|
314
|
+
|
|
315
|
+
<div class="rounded-md border border-slate-800 p-3">
|
|
316
|
+
<p class="mb-2 text-sm font-medium">Add a fragment</p>
|
|
317
|
+
<div class="flex flex-col gap-2">
|
|
318
|
+
<UInput v-model="draft.title" placeholder="Title" />
|
|
319
|
+
<UInput v-model="draft.summary" placeholder="One-line summary (used by the selector)" />
|
|
320
|
+
<UTextarea
|
|
321
|
+
v-model="draft.body"
|
|
322
|
+
placeholder="Guidance body (injected into the prompt)"
|
|
323
|
+
:rows="4"
|
|
324
|
+
/>
|
|
325
|
+
<UInput v-model="draft.tags" placeholder="Tags, comma-separated (e.g. backend, db)" />
|
|
326
|
+
<UButton
|
|
327
|
+
icon="i-lucide-plus"
|
|
328
|
+
size="sm"
|
|
329
|
+
:disabled="!draftValid"
|
|
330
|
+
:loading="library.loading"
|
|
331
|
+
class="self-start"
|
|
332
|
+
@click="createFragment"
|
|
333
|
+
>
|
|
334
|
+
Add fragment
|
|
335
|
+
</UButton>
|
|
336
|
+
</div>
|
|
337
|
+
</div>
|
|
338
|
+
</div>
|
|
339
|
+
|
|
340
|
+
<!-- Document-backed (living) fragments -->
|
|
341
|
+
<div v-else-if="tab === 'documents'" class="flex flex-col gap-3">
|
|
342
|
+
<p class="text-xs text-slate-500">
|
|
343
|
+
Link a Confluence/Notion page or a GitHub file as a best-practice fragment. Its guidance is
|
|
344
|
+
re-resolved from the source at run time — edit the doc and the next agent run follows the
|
|
345
|
+
new version (no re-import).
|
|
346
|
+
</p>
|
|
347
|
+
|
|
348
|
+
<div
|
|
349
|
+
v-for="f in documentFragments"
|
|
350
|
+
:key="f.id"
|
|
351
|
+
class="flex items-start gap-2 rounded-md border border-slate-800 bg-slate-900/60 p-3"
|
|
352
|
+
>
|
|
353
|
+
<UIcon name="i-lucide-radio" class="mt-0.5 h-4 w-4 text-emerald-400" />
|
|
354
|
+
<div class="min-w-0">
|
|
355
|
+
<div class="flex items-center gap-2">
|
|
356
|
+
<span class="font-medium text-slate-100">{{ f.title }}</span>
|
|
357
|
+
<UBadge size="xs" color="success" variant="subtle">
|
|
358
|
+
{{ f.documentRef?.source }}
|
|
359
|
+
</UBadge>
|
|
360
|
+
</div>
|
|
361
|
+
<p class="text-sm text-slate-400">{{ f.summary }}</p>
|
|
362
|
+
<p v-if="f.resolvedAt" class="text-[11px] text-slate-500">
|
|
363
|
+
last resolved {{ new Date(f.resolvedAt).toLocaleString() }}
|
|
364
|
+
</p>
|
|
365
|
+
</div>
|
|
366
|
+
<div class="ml-auto flex gap-1">
|
|
367
|
+
<UButton
|
|
368
|
+
icon="i-lucide-refresh-cw"
|
|
369
|
+
size="xs"
|
|
370
|
+
variant="ghost"
|
|
371
|
+
:loading="library.loading"
|
|
372
|
+
title="Re-resolve from source now"
|
|
373
|
+
@click="refreshFragment(f.id)"
|
|
374
|
+
/>
|
|
375
|
+
<UButton
|
|
376
|
+
icon="i-lucide-trash-2"
|
|
377
|
+
size="xs"
|
|
378
|
+
color="error"
|
|
379
|
+
variant="ghost"
|
|
380
|
+
@click="removeFragment(f.id)"
|
|
381
|
+
/>
|
|
382
|
+
</div>
|
|
383
|
+
</div>
|
|
384
|
+
<p v-if="!documentFragments.length" class="text-sm text-slate-500">
|
|
385
|
+
No document-backed fragments yet. Link one below.
|
|
386
|
+
</p>
|
|
387
|
+
|
|
388
|
+
<div class="rounded-md border border-slate-800 p-3">
|
|
389
|
+
<p class="mb-2 text-sm font-medium">Link a document</p>
|
|
390
|
+
<div v-if="docLinkDisabled" class="text-sm text-slate-500">
|
|
391
|
+
Create a board with a connected document source first — account-level document fragments
|
|
392
|
+
are fetched through one of this account's boards.
|
|
393
|
+
</div>
|
|
394
|
+
<div v-else-if="!documents.connectedSources.length" class="text-sm text-slate-500">
|
|
395
|
+
Connect a document source (Confluence, Notion or GitHub) under Integrations first.
|
|
396
|
+
</div>
|
|
397
|
+
<div v-else class="flex flex-col gap-2">
|
|
398
|
+
<div class="flex flex-wrap gap-2">
|
|
399
|
+
<UButton
|
|
400
|
+
v-for="s in documents.connectedSources"
|
|
401
|
+
:key="s.source"
|
|
402
|
+
size="xs"
|
|
403
|
+
:color="docDraft.source === s.source ? 'primary' : 'neutral'"
|
|
404
|
+
:variant="docDraft.source === s.source ? 'solid' : 'outline'"
|
|
405
|
+
@click="docDraft.source = s.source"
|
|
406
|
+
>
|
|
407
|
+
{{ s.label }}
|
|
408
|
+
</UButton>
|
|
409
|
+
</div>
|
|
410
|
+
<UInput
|
|
411
|
+
v-model="docDraft.ref"
|
|
412
|
+
placeholder="Page id or URL (e.g. a Confluence/Notion page or GitHub file URL)"
|
|
413
|
+
/>
|
|
414
|
+
<UInput v-model="docDraft.tags" placeholder="Tags, comma-separated (optional)" />
|
|
415
|
+
<UButton
|
|
416
|
+
icon="i-lucide-link"
|
|
417
|
+
size="sm"
|
|
418
|
+
:disabled="!docDraftValid"
|
|
419
|
+
:loading="library.loading"
|
|
420
|
+
class="self-start"
|
|
421
|
+
@click="linkDocumentFragment"
|
|
422
|
+
>
|
|
423
|
+
Link as living fragment
|
|
424
|
+
</UButton>
|
|
425
|
+
</div>
|
|
426
|
+
</div>
|
|
427
|
+
</div>
|
|
428
|
+
|
|
429
|
+
<!-- Repo sources -->
|
|
430
|
+
<div v-else class="flex flex-col gap-3">
|
|
431
|
+
<div
|
|
432
|
+
v-for="s in library.sources"
|
|
433
|
+
:key="s.id"
|
|
434
|
+
class="flex items-center gap-2 rounded-md border border-slate-800 bg-slate-900/60 p-3"
|
|
435
|
+
>
|
|
436
|
+
<UIcon name="i-lucide-git-branch" class="h-4 w-4 text-slate-400" />
|
|
437
|
+
<div class="min-w-0">
|
|
438
|
+
<span class="font-mono text-sm text-slate-100">
|
|
439
|
+
{{ s.repoOwner }}/{{ s.repoName
|
|
440
|
+
}}<span class="text-slate-500">/{{ s.dirPath || '' }}</span>
|
|
441
|
+
</span>
|
|
442
|
+
<p class="text-xs text-slate-500">
|
|
443
|
+
{{ s.lastSyncedAt ? 'synced' : 'never synced' }} · ref {{ s.gitRef }}
|
|
444
|
+
</p>
|
|
445
|
+
</div>
|
|
446
|
+
<UBadge
|
|
447
|
+
v-if="library.sourceChanges[s.id]"
|
|
448
|
+
size="xs"
|
|
449
|
+
color="warning"
|
|
450
|
+
variant="subtle"
|
|
451
|
+
class="ml-auto"
|
|
452
|
+
>
|
|
453
|
+
{{ library.sourceChanges[s.id] }} change(s)
|
|
454
|
+
</UBadge>
|
|
455
|
+
<div class="ml-auto flex gap-1">
|
|
456
|
+
<UButton
|
|
457
|
+
icon="i-lucide-search-check"
|
|
458
|
+
size="xs"
|
|
459
|
+
variant="ghost"
|
|
460
|
+
@click="checkSource(s.id)"
|
|
461
|
+
/>
|
|
462
|
+
<UButton
|
|
463
|
+
icon="i-lucide-refresh-cw"
|
|
464
|
+
size="xs"
|
|
465
|
+
variant="ghost"
|
|
466
|
+
:loading="library.loading"
|
|
467
|
+
@click="syncSource(s.id)"
|
|
468
|
+
/>
|
|
469
|
+
<UButton
|
|
470
|
+
icon="i-lucide-unplug"
|
|
471
|
+
size="xs"
|
|
472
|
+
color="error"
|
|
473
|
+
variant="ghost"
|
|
474
|
+
@click="unlinkSource(s.id)"
|
|
475
|
+
/>
|
|
476
|
+
</div>
|
|
477
|
+
</div>
|
|
478
|
+
<p v-if="!library.sources.length" class="text-sm text-slate-500">
|
|
479
|
+
No linked guideline repos. Link one below to import its Markdown files as fragments.
|
|
480
|
+
</p>
|
|
481
|
+
|
|
482
|
+
<div class="rounded-md border border-slate-800 p-3">
|
|
483
|
+
<p class="mb-2 text-sm font-medium">Link a guideline repo</p>
|
|
484
|
+
<div class="flex flex-col gap-2">
|
|
485
|
+
<div class="flex gap-2">
|
|
486
|
+
<UInput v-model="sourceDraft.repoOwner" placeholder="owner" class="flex-1" />
|
|
487
|
+
<UInput v-model="sourceDraft.repoName" placeholder="repo" class="flex-1" />
|
|
488
|
+
</div>
|
|
489
|
+
<div class="flex gap-2">
|
|
490
|
+
<UInput
|
|
491
|
+
v-model="sourceDraft.dirPath"
|
|
492
|
+
placeholder="dir path (e.g. guidelines)"
|
|
493
|
+
class="flex-1"
|
|
494
|
+
/>
|
|
495
|
+
<UInput v-model="sourceDraft.gitRef" placeholder="ref (default HEAD)" class="flex-1" />
|
|
496
|
+
</div>
|
|
497
|
+
<UButton
|
|
498
|
+
icon="i-lucide-link"
|
|
499
|
+
size="sm"
|
|
500
|
+
:disabled="!sourceValid"
|
|
501
|
+
:loading="library.loading"
|
|
502
|
+
class="self-start"
|
|
503
|
+
@click="linkSource"
|
|
504
|
+
>
|
|
505
|
+
Link & sync
|
|
506
|
+
</UButton>
|
|
507
|
+
</div>
|
|
508
|
+
</div>
|
|
509
|
+
</div>
|
|
510
|
+
</div>
|
|
511
|
+
</template>
|