@cat-factory/app 0.74.3 → 0.75.2
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/board/AgentFailureCard.vue +8 -9
- package/app/components/board/AgentFailureHistory.vue +60 -0
- package/app/components/board/FailureDetail.vue +27 -0
- package/app/components/fragments/FragmentLibraryManager.vue +410 -283
- package/app/components/github/GitHubRepoSearchSelect.vue +118 -0
- package/app/components/panels/inspector/ServiceFragments.vue +7 -4
- package/app/components/panels/inspector/TaskExecution.vue +8 -0
- package/app/components/panels/inspector/TaskStructure.vue +10 -4
- package/app/components/settings/ServiceFragmentDefaultsPanel.vue +6 -5
- package/app/stores/agentRuns.ts +10 -0
- package/app/stores/fragmentLibrary.ts +4 -0
- package/app/stores/fragments.ts +30 -7
- package/app/stores/workspace.ts +5 -0
- package/i18n/locales/en.json +15 -4
- package/i18n/locales/es.json +12 -4
- package/i18n/locales/fr.json +12 -4
- package/i18n/locales/he.json +12 -4
- package/i18n/locales/ja.json +12 -4
- package/i18n/locales/pl.json +12 -4
- package/i18n/locales/tr.json +12 -4
- package/i18n/locales/uk.json +12 -4
- package/package.json +2 -2
|
@@ -7,8 +7,15 @@
|
|
|
7
7
|
// run. The account scope has no resolved/merged catalog and fetches document
|
|
8
8
|
// fragments through `viaWorkspaceId` (document-source credentials are per-workspace).
|
|
9
9
|
import { computed, ref, watch } from 'vue'
|
|
10
|
-
import type {
|
|
10
|
+
import type {
|
|
11
|
+
DocumentSourceKind,
|
|
12
|
+
FragmentOwnerKind,
|
|
13
|
+
GitHubAvailableRepo,
|
|
14
|
+
ResolvedFragment,
|
|
15
|
+
} from '~/types/domain'
|
|
11
16
|
import { useFragmentLibrary, useFragmentLibraryStore } from '~/stores/fragmentLibrary'
|
|
17
|
+
import GitHubRepoSearchSelect from '~/components/github/GitHubRepoSearchSelect.vue'
|
|
18
|
+
import RepoTreeBrowser from '~/components/github/RepoTreeBrowser.vue'
|
|
12
19
|
|
|
13
20
|
const props = withDefaults(
|
|
14
21
|
defineProps<{
|
|
@@ -29,6 +36,7 @@ const library =
|
|
|
29
36
|
? useFragmentLibraryStore()
|
|
30
37
|
: useFragmentLibrary(props.kind, props.ownerId)
|
|
31
38
|
const documents = useDocumentsStore()
|
|
39
|
+
const github = useGitHubStore()
|
|
32
40
|
const toast = useToast()
|
|
33
41
|
const { t, d } = useI18n()
|
|
34
42
|
const { confirm } = useConfirm()
|
|
@@ -50,10 +58,17 @@ watch(
|
|
|
50
58
|
() => {
|
|
51
59
|
void library.probe()
|
|
52
60
|
void documents.probe()
|
|
61
|
+
// The GitHub pickers (repo search + tree browser) need the active board's
|
|
62
|
+
// installation state; probe once so they light up when the App is connected.
|
|
63
|
+
void github.probe()
|
|
53
64
|
},
|
|
54
65
|
{ immediate: true },
|
|
55
66
|
)
|
|
56
67
|
|
|
68
|
+
// The rich GitHub pickers reuse the active board's App installation. When it isn't
|
|
69
|
+
// connected (or the integration is off) both forms fall back to manual text entry.
|
|
70
|
+
const githubReady = computed(() => github.available === true && github.connected)
|
|
71
|
+
|
|
57
72
|
type Tab = 'catalog' | 'authored' | 'documents' | 'sources'
|
|
58
73
|
const tabs = computed<Tab[]>(() =>
|
|
59
74
|
props.showCatalog
|
|
@@ -147,6 +162,40 @@ const docDraftValid = computed(
|
|
|
147
162
|
() => !docLinkDisabled.value && docDraft.value.source && docDraft.value.ref.trim(),
|
|
148
163
|
)
|
|
149
164
|
|
|
165
|
+
// ---- GitHub file picker (documents tab) -----------------------------------
|
|
166
|
+
// For a GitHub source, let the user search a repo + browse to the file instead of
|
|
167
|
+
// hand-typing a `owner/repo:path` ref. Picking a file fills `docDraft.ref` (still
|
|
168
|
+
// editable, so pasting a URL/shorthand keeps working). Only offered when the App is
|
|
169
|
+
// connected; other sources (Confluence/Notion) keep the free-text ref field.
|
|
170
|
+
const isGithubDoc = computed(() => docDraft.value.source === 'github')
|
|
171
|
+
const showGithubDocPicker = computed(() => isGithubDoc.value && githubReady.value)
|
|
172
|
+
const docRepoId = ref<number | undefined>(undefined)
|
|
173
|
+
const docRepo = ref<GitHubAvailableRepo | undefined>(undefined)
|
|
174
|
+
const docFilePath = ref<string | undefined>(undefined)
|
|
175
|
+
|
|
176
|
+
// Reset the picker (and the derived ref) whenever the selected source changes.
|
|
177
|
+
watch(
|
|
178
|
+
() => docDraft.value.source,
|
|
179
|
+
() => {
|
|
180
|
+
docRepoId.value = undefined
|
|
181
|
+
docRepo.value = undefined
|
|
182
|
+
docFilePath.value = undefined
|
|
183
|
+
docDraft.value.ref = ''
|
|
184
|
+
},
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
// A new repo selection clears the previously-browsed file.
|
|
188
|
+
watch(docRepoId, () => {
|
|
189
|
+
docFilePath.value = undefined
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
// Derive the canonical `owner/repo:path` ref the GitHub docs provider expects.
|
|
193
|
+
watch([docRepo, docFilePath], () => {
|
|
194
|
+
if (docRepo.value && docFilePath.value) {
|
|
195
|
+
docDraft.value.ref = `${docRepo.value.owner}/${docRepo.value.name}:${docFilePath.value}`
|
|
196
|
+
}
|
|
197
|
+
})
|
|
198
|
+
|
|
150
199
|
/** This tier's existing document-backed fragments. */
|
|
151
200
|
const documentFragments = computed(() => library.fragments.filter((f) => f.documentRef))
|
|
152
201
|
|
|
@@ -178,21 +227,50 @@ async function refreshFragment(id: string) {
|
|
|
178
227
|
}
|
|
179
228
|
|
|
180
229
|
// ---- repo sources ----------------------------------------------------------
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
)
|
|
230
|
+
// When the App is connected the user searches a repo + browses to the guideline
|
|
231
|
+
// directory; otherwise the manual owner/name/dir fields are the fallback.
|
|
232
|
+
const sourceRepoId = ref<number | undefined>(undefined)
|
|
233
|
+
const sourceRepo = ref<GitHubAvailableRepo | undefined>(undefined)
|
|
234
|
+
const sourceDir = ref<string | undefined>(undefined)
|
|
235
|
+
const sourceRef = ref('')
|
|
236
|
+
const manualSource = ref({ repoOwner: '', repoName: '', dirPath: '' })
|
|
237
|
+
|
|
238
|
+
// A new repo selection clears the previously-browsed directory.
|
|
239
|
+
watch(sourceRepoId, () => {
|
|
240
|
+
sourceDir.value = undefined
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
const sourceOwnerName = computed<{ owner: string; name: string } | null>(() => {
|
|
244
|
+
if (githubReady.value) {
|
|
245
|
+
return sourceRepo.value ? { owner: sourceRepo.value.owner, name: sourceRepo.value.name } : null
|
|
246
|
+
}
|
|
247
|
+
const owner = manualSource.value.repoOwner.trim()
|
|
248
|
+
const name = manualSource.value.repoName.trim()
|
|
249
|
+
return owner && name ? { owner, name } : null
|
|
250
|
+
})
|
|
251
|
+
const sourceValid = computed(() => sourceOwnerName.value !== null)
|
|
252
|
+
|
|
253
|
+
function resetSourceDraft() {
|
|
254
|
+
sourceRepoId.value = undefined
|
|
255
|
+
sourceRepo.value = undefined
|
|
256
|
+
sourceDir.value = undefined
|
|
257
|
+
sourceRef.value = ''
|
|
258
|
+
manualSource.value = { repoOwner: '', repoName: '', dirPath: '' }
|
|
259
|
+
}
|
|
185
260
|
|
|
186
261
|
async function linkSource() {
|
|
187
|
-
|
|
262
|
+
const ownerName = sourceOwnerName.value
|
|
263
|
+
if (!ownerName) return
|
|
264
|
+
const dirPath =
|
|
265
|
+
(githubReady.value ? sourceDir.value : manualSource.value.dirPath.trim()) || undefined
|
|
188
266
|
try {
|
|
189
267
|
const source = await library.linkSource({
|
|
190
|
-
repoOwner:
|
|
191
|
-
repoName:
|
|
192
|
-
dirPath
|
|
193
|
-
gitRef:
|
|
268
|
+
repoOwner: ownerName.owner,
|
|
269
|
+
repoName: ownerName.name,
|
|
270
|
+
dirPath,
|
|
271
|
+
gitRef: sourceRef.value.trim() || undefined,
|
|
194
272
|
})
|
|
195
|
-
|
|
273
|
+
resetSourceDraft()
|
|
196
274
|
await library.syncSource(source.id)
|
|
197
275
|
toast.add({ title: t('fragments.toast.sourceLinked'), icon: 'i-lucide-git-branch' })
|
|
198
276
|
} catch (e) {
|
|
@@ -242,315 +320,364 @@ async function unlinkSource(id: string) {
|
|
|
242
320
|
|
|
243
321
|
<template>
|
|
244
322
|
<div class="flex flex-col gap-4">
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
</p>
|
|
253
|
-
|
|
254
|
-
<div class="flex gap-2">
|
|
255
|
-
<UButton
|
|
256
|
-
v-for="t in tabs"
|
|
257
|
-
:key="t"
|
|
258
|
-
:color="tab === t ? 'primary' : 'neutral'"
|
|
259
|
-
:variant="tab === t ? 'solid' : 'ghost'"
|
|
260
|
-
size="sm"
|
|
261
|
-
@click="tab = t"
|
|
262
|
-
>
|
|
263
|
-
{{ tabLabel(t) }}
|
|
264
|
-
</UButton>
|
|
323
|
+
<!-- The library is opt-out; if a deployment disabled it, don't offer forms that
|
|
324
|
+
would fail with a raw 503 — say so instead (any entry point lands here). -->
|
|
325
|
+
<div
|
|
326
|
+
v-if="library.available === false"
|
|
327
|
+
class="rounded-md border border-slate-800 bg-slate-900/40 p-3 text-sm text-slate-400"
|
|
328
|
+
>
|
|
329
|
+
{{ t('fragments.unavailable') }}
|
|
265
330
|
</div>
|
|
266
331
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
)
|
|
276
|
-
}}
|
|
332
|
+
<template v-else>
|
|
333
|
+
<p class="text-sm text-slate-400">
|
|
334
|
+
<template v-if="isWorkspace">
|
|
335
|
+
{{ t('fragments.intro.workspace') }}
|
|
336
|
+
</template>
|
|
337
|
+
<template v-else>
|
|
338
|
+
{{ t('fragments.intro.account') }}
|
|
339
|
+
</template>
|
|
277
340
|
</p>
|
|
278
|
-
<div
|
|
279
|
-
v-for="f in library.resolved"
|
|
280
|
-
:key="f.id"
|
|
281
|
-
class="rounded-md border border-slate-800 bg-slate-900/60 p-3"
|
|
282
|
-
>
|
|
283
|
-
<div class="flex items-center gap-2">
|
|
284
|
-
<span class="font-medium text-slate-100">{{ f.title }}</span>
|
|
285
|
-
<UBadge size="xs" :color="tierColor[f.tier]" variant="subtle">
|
|
286
|
-
{{ tierLabel[f.tier] }}
|
|
287
|
-
</UBadge>
|
|
288
|
-
<UBadge
|
|
289
|
-
v-if="f.documentRef"
|
|
290
|
-
size="xs"
|
|
291
|
-
color="success"
|
|
292
|
-
variant="subtle"
|
|
293
|
-
icon="i-lucide-radio"
|
|
294
|
-
>
|
|
295
|
-
{{ t('fragments.catalog.live', { source: f.documentRef.source }) }}
|
|
296
|
-
</UBadge>
|
|
297
|
-
<span class="ms-auto font-mono text-[11px] text-slate-500">{{ f.id }}</span>
|
|
298
|
-
</div>
|
|
299
|
-
<p class="mt-1 text-sm text-slate-400">{{ f.summary }}</p>
|
|
300
|
-
<div v-if="f.tags?.length" class="mt-1 flex flex-wrap gap-1">
|
|
301
|
-
<UBadge v-for="tag in f.tags" :key="tag" size="xs" variant="outline" color="neutral">
|
|
302
|
-
{{ tag }}
|
|
303
|
-
</UBadge>
|
|
304
|
-
</div>
|
|
305
|
-
</div>
|
|
306
|
-
</div>
|
|
307
341
|
|
|
308
|
-
|
|
309
|
-
<div v-else-if="tab === 'authored'" class="flex flex-col gap-3">
|
|
310
|
-
<div
|
|
311
|
-
v-for="f in library.fragments"
|
|
312
|
-
:key="f.id"
|
|
313
|
-
class="flex items-start gap-2 rounded-md border border-slate-800 bg-slate-900/60 p-3"
|
|
314
|
-
>
|
|
315
|
-
<div class="min-w-0">
|
|
316
|
-
<div class="flex items-center gap-2">
|
|
317
|
-
<span class="font-medium text-slate-100">{{ f.title }}</span>
|
|
318
|
-
<UBadge v-if="f.source" size="xs" color="info" variant="subtle">{{
|
|
319
|
-
t('fragments.authored.fromRepo')
|
|
320
|
-
}}</UBadge>
|
|
321
|
-
</div>
|
|
322
|
-
<p class="text-sm text-slate-400">{{ f.summary }}</p>
|
|
323
|
-
</div>
|
|
342
|
+
<div class="flex gap-2">
|
|
324
343
|
<UButton
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
color="
|
|
328
|
-
variant="ghost"
|
|
329
|
-
|
|
330
|
-
@click="
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
{{
|
|
335
|
-
isWorkspace
|
|
336
|
-
? t('fragments.authored.empty.workspace')
|
|
337
|
-
: t('fragments.authored.empty.account')
|
|
338
|
-
}}
|
|
339
|
-
</p>
|
|
340
|
-
|
|
341
|
-
<div class="rounded-md border border-slate-800 p-3">
|
|
342
|
-
<p class="mb-2 text-sm font-medium">{{ t('fragments.authored.addTitle') }}</p>
|
|
343
|
-
<div class="flex flex-col gap-2">
|
|
344
|
-
<UInput v-model="draft.title" :placeholder="t('fragments.authored.titlePlaceholder')" />
|
|
345
|
-
<UInput
|
|
346
|
-
v-model="draft.summary"
|
|
347
|
-
:placeholder="t('fragments.authored.summaryPlaceholder')"
|
|
348
|
-
/>
|
|
349
|
-
<UTextarea
|
|
350
|
-
v-model="draft.body"
|
|
351
|
-
:placeholder="t('fragments.authored.bodyPlaceholder')"
|
|
352
|
-
:rows="4"
|
|
353
|
-
/>
|
|
354
|
-
<UInput v-model="draft.tags" :placeholder="t('fragments.authored.tagsPlaceholder')" />
|
|
355
|
-
<UButton
|
|
356
|
-
icon="i-lucide-plus"
|
|
357
|
-
size="sm"
|
|
358
|
-
:disabled="!draftValid"
|
|
359
|
-
:loading="library.loading"
|
|
360
|
-
class="self-start"
|
|
361
|
-
@click="createFragment"
|
|
362
|
-
>
|
|
363
|
-
{{ t('fragments.authored.add') }}
|
|
364
|
-
</UButton>
|
|
365
|
-
</div>
|
|
344
|
+
v-for="t in tabs"
|
|
345
|
+
:key="t"
|
|
346
|
+
:color="tab === t ? 'primary' : 'neutral'"
|
|
347
|
+
:variant="tab === t ? 'solid' : 'ghost'"
|
|
348
|
+
size="sm"
|
|
349
|
+
@click="tab = t"
|
|
350
|
+
>
|
|
351
|
+
{{ tabLabel(t) }}
|
|
352
|
+
</UButton>
|
|
366
353
|
</div>
|
|
367
|
-
</div>
|
|
368
354
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
<
|
|
381
|
-
|
|
355
|
+
<!-- Resolved (merged) catalog — workspace scope only -->
|
|
356
|
+
<div v-if="tab === 'catalog'" class="flex flex-col gap-2">
|
|
357
|
+
<p class="text-xs text-slate-500">
|
|
358
|
+
{{
|
|
359
|
+
t(
|
|
360
|
+
'fragments.catalog.summary',
|
|
361
|
+
{ count: library.resolved.length, builtin: library.builtinCount },
|
|
362
|
+
library.resolved.length,
|
|
363
|
+
)
|
|
364
|
+
}}
|
|
365
|
+
</p>
|
|
366
|
+
<div
|
|
367
|
+
v-for="f in library.resolved"
|
|
368
|
+
:key="f.id"
|
|
369
|
+
class="rounded-md border border-slate-800 bg-slate-900/60 p-3"
|
|
370
|
+
>
|
|
382
371
|
<div class="flex items-center gap-2">
|
|
383
372
|
<span class="font-medium text-slate-100">{{ f.title }}</span>
|
|
384
|
-
<UBadge size="xs" color="
|
|
385
|
-
{{ f.
|
|
373
|
+
<UBadge size="xs" :color="tierColor[f.tier]" variant="subtle">
|
|
374
|
+
{{ tierLabel[f.tier] }}
|
|
375
|
+
</UBadge>
|
|
376
|
+
<UBadge
|
|
377
|
+
v-if="f.documentRef"
|
|
378
|
+
size="xs"
|
|
379
|
+
color="success"
|
|
380
|
+
variant="subtle"
|
|
381
|
+
icon="i-lucide-radio"
|
|
382
|
+
>
|
|
383
|
+
{{ t('fragments.catalog.live', { source: f.documentRef.source }) }}
|
|
384
|
+
</UBadge>
|
|
385
|
+
<span class="ms-auto font-mono text-[11px] text-slate-500">{{ f.id }}</span>
|
|
386
|
+
</div>
|
|
387
|
+
<p class="mt-1 text-sm text-slate-400">{{ f.summary }}</p>
|
|
388
|
+
<div v-if="f.tags?.length" class="mt-1 flex flex-wrap gap-1">
|
|
389
|
+
<UBadge v-for="tag in f.tags" :key="tag" size="xs" variant="outline" color="neutral">
|
|
390
|
+
{{ tag }}
|
|
386
391
|
</UBadge>
|
|
387
392
|
</div>
|
|
388
|
-
<p class="text-sm text-slate-400">{{ f.summary }}</p>
|
|
389
|
-
<p v-if="f.resolvedAt" class="text-[11px] text-slate-500">
|
|
390
|
-
{{ t('fragments.documents.lastResolved', { date: d(new Date(f.resolvedAt), 'long') }) }}
|
|
391
|
-
</p>
|
|
392
393
|
</div>
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
394
|
+
</div>
|
|
395
|
+
|
|
396
|
+
<!-- Hand-authored (this tier) -->
|
|
397
|
+
<div v-else-if="tab === 'authored'" class="flex flex-col gap-3">
|
|
398
|
+
<div
|
|
399
|
+
v-for="f in library.fragments"
|
|
400
|
+
:key="f.id"
|
|
401
|
+
class="flex items-start gap-2 rounded-md border border-slate-800 bg-slate-900/60 p-3"
|
|
402
|
+
>
|
|
403
|
+
<div class="min-w-0">
|
|
404
|
+
<div class="flex items-center gap-2">
|
|
405
|
+
<span class="font-medium text-slate-100">{{ f.title }}</span>
|
|
406
|
+
<UBadge v-if="f.source" size="xs" color="info" variant="subtle">{{
|
|
407
|
+
t('fragments.authored.fromRepo')
|
|
408
|
+
}}</UBadge>
|
|
409
|
+
</div>
|
|
410
|
+
<p class="text-sm text-slate-400">{{ f.summary }}</p>
|
|
411
|
+
</div>
|
|
402
412
|
<UButton
|
|
403
413
|
icon="i-lucide-trash-2"
|
|
404
414
|
size="xs"
|
|
405
415
|
color="error"
|
|
406
416
|
variant="ghost"
|
|
417
|
+
class="ms-auto"
|
|
407
418
|
@click="removeFragment(f.id)"
|
|
408
419
|
/>
|
|
409
420
|
</div>
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
421
|
+
<p v-if="!library.fragments.length" class="text-sm text-slate-500">
|
|
422
|
+
{{
|
|
423
|
+
isWorkspace
|
|
424
|
+
? t('fragments.authored.empty.workspace')
|
|
425
|
+
: t('fragments.authored.empty.account')
|
|
426
|
+
}}
|
|
427
|
+
</p>
|
|
414
428
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
429
|
+
<div class="rounded-md border border-slate-800 p-3">
|
|
430
|
+
<p class="mb-2 text-sm font-medium">{{ t('fragments.authored.addTitle') }}</p>
|
|
431
|
+
<div class="flex flex-col gap-2">
|
|
432
|
+
<UInput v-model="draft.title" :placeholder="t('fragments.authored.titlePlaceholder')" />
|
|
433
|
+
<UInput
|
|
434
|
+
v-model="draft.summary"
|
|
435
|
+
:placeholder="t('fragments.authored.summaryPlaceholder')"
|
|
436
|
+
/>
|
|
437
|
+
<UTextarea
|
|
438
|
+
v-model="draft.body"
|
|
439
|
+
:placeholder="t('fragments.authored.bodyPlaceholder')"
|
|
440
|
+
:rows="4"
|
|
441
|
+
/>
|
|
442
|
+
<UInput v-model="draft.tags" :placeholder="t('fragments.authored.tagsPlaceholder')" />
|
|
425
443
|
<UButton
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
:
|
|
430
|
-
|
|
431
|
-
@click="
|
|
444
|
+
icon="i-lucide-plus"
|
|
445
|
+
size="sm"
|
|
446
|
+
:disabled="!draftValid"
|
|
447
|
+
:loading="library.loading"
|
|
448
|
+
class="self-start"
|
|
449
|
+
@click="createFragment"
|
|
432
450
|
>
|
|
433
|
-
{{
|
|
451
|
+
{{ t('fragments.authored.add') }}
|
|
434
452
|
</UButton>
|
|
435
453
|
</div>
|
|
436
|
-
<UInput v-model="docDraft.ref" :placeholder="t('fragments.documents.refPlaceholder')" />
|
|
437
|
-
<UInput v-model="docDraft.tags" :placeholder="t('fragments.documents.tagsPlaceholder')" />
|
|
438
|
-
<UButton
|
|
439
|
-
icon="i-lucide-link"
|
|
440
|
-
size="sm"
|
|
441
|
-
:disabled="!docDraftValid"
|
|
442
|
-
:loading="library.loading"
|
|
443
|
-
class="self-start"
|
|
444
|
-
@click="linkDocumentFragment"
|
|
445
|
-
>
|
|
446
|
-
{{ t('fragments.documents.link') }}
|
|
447
|
-
</UButton>
|
|
448
454
|
</div>
|
|
449
455
|
</div>
|
|
450
|
-
</div>
|
|
451
456
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
class="flex items-center gap-2 rounded-md border border-slate-800 bg-slate-900/60 p-3"
|
|
458
|
-
>
|
|
459
|
-
<UIcon name="i-lucide-git-branch" class="h-4 w-4 text-slate-400" />
|
|
460
|
-
<div class="min-w-0">
|
|
461
|
-
<span class="font-mono text-sm text-slate-100">
|
|
462
|
-
{{ s.repoOwner }}/{{ s.repoName
|
|
463
|
-
}}<span class="text-slate-500">/{{ s.dirPath || '' }}</span>
|
|
464
|
-
</span>
|
|
465
|
-
<p class="text-xs text-slate-500">
|
|
466
|
-
{{
|
|
467
|
-
s.lastSyncedAt
|
|
468
|
-
? t('fragments.sources.metaSynced', { ref: s.gitRef })
|
|
469
|
-
: t('fragments.sources.metaNever', { ref: s.gitRef })
|
|
470
|
-
}}
|
|
471
|
-
</p>
|
|
472
|
-
</div>
|
|
473
|
-
<UBadge
|
|
474
|
-
v-if="library.sourceChanges[s.id]"
|
|
475
|
-
size="xs"
|
|
476
|
-
color="warning"
|
|
477
|
-
variant="subtle"
|
|
478
|
-
class="ms-auto"
|
|
479
|
-
>
|
|
480
|
-
{{
|
|
481
|
-
t(
|
|
482
|
-
'fragments.sources.changes',
|
|
483
|
-
{ count: library.sourceChanges[s.id] },
|
|
484
|
-
library.sourceChanges[s.id] ?? 0,
|
|
485
|
-
)
|
|
486
|
-
}}
|
|
487
|
-
</UBadge>
|
|
488
|
-
<div class="ms-auto flex gap-1">
|
|
489
|
-
<UButton
|
|
490
|
-
icon="i-lucide-search-check"
|
|
491
|
-
size="xs"
|
|
492
|
-
variant="ghost"
|
|
493
|
-
@click="checkSource(s.id)"
|
|
494
|
-
/>
|
|
495
|
-
<UButton
|
|
496
|
-
icon="i-lucide-refresh-cw"
|
|
497
|
-
size="xs"
|
|
498
|
-
variant="ghost"
|
|
499
|
-
:loading="library.loading"
|
|
500
|
-
@click="syncSource(s.id)"
|
|
501
|
-
/>
|
|
502
|
-
<UButton
|
|
503
|
-
icon="i-lucide-unplug"
|
|
504
|
-
size="xs"
|
|
505
|
-
color="error"
|
|
506
|
-
variant="ghost"
|
|
507
|
-
@click="unlinkSource(s.id)"
|
|
508
|
-
/>
|
|
509
|
-
</div>
|
|
510
|
-
</div>
|
|
511
|
-
<p v-if="!library.sources.length" class="text-sm text-slate-500">
|
|
512
|
-
{{ t('fragments.sources.empty') }}
|
|
513
|
-
</p>
|
|
457
|
+
<!-- Document-backed (living) fragments -->
|
|
458
|
+
<div v-else-if="tab === 'documents'" class="flex flex-col gap-3">
|
|
459
|
+
<p class="text-xs text-slate-500">
|
|
460
|
+
{{ t('fragments.documents.intro') }}
|
|
461
|
+
</p>
|
|
514
462
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
463
|
+
<div
|
|
464
|
+
v-for="f in documentFragments"
|
|
465
|
+
:key="f.id"
|
|
466
|
+
class="flex items-start gap-2 rounded-md border border-slate-800 bg-slate-900/60 p-3"
|
|
467
|
+
>
|
|
468
|
+
<UIcon name="i-lucide-radio" class="mt-0.5 h-4 w-4 text-emerald-400" />
|
|
469
|
+
<div class="min-w-0">
|
|
470
|
+
<div class="flex items-center gap-2">
|
|
471
|
+
<span class="font-medium text-slate-100">{{ f.title }}</span>
|
|
472
|
+
<UBadge size="xs" color="success" variant="subtle">
|
|
473
|
+
{{ f.documentRef?.source }}
|
|
474
|
+
</UBadge>
|
|
475
|
+
</div>
|
|
476
|
+
<p class="text-sm text-slate-400">{{ f.summary }}</p>
|
|
477
|
+
<p v-if="f.resolvedAt" class="text-[11px] text-slate-500">
|
|
478
|
+
{{
|
|
479
|
+
t('fragments.documents.lastResolved', { date: d(new Date(f.resolvedAt), 'long') })
|
|
480
|
+
}}
|
|
481
|
+
</p>
|
|
482
|
+
</div>
|
|
483
|
+
<div class="ms-auto flex gap-1">
|
|
484
|
+
<UButton
|
|
485
|
+
icon="i-lucide-refresh-cw"
|
|
486
|
+
size="xs"
|
|
487
|
+
variant="ghost"
|
|
488
|
+
:loading="library.loading"
|
|
489
|
+
:title="t('fragments.documents.refreshTitle')"
|
|
490
|
+
@click="refreshFragment(f.id)"
|
|
523
491
|
/>
|
|
524
|
-
<
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
492
|
+
<UButton
|
|
493
|
+
icon="i-lucide-trash-2"
|
|
494
|
+
size="xs"
|
|
495
|
+
color="error"
|
|
496
|
+
variant="ghost"
|
|
497
|
+
@click="removeFragment(f.id)"
|
|
528
498
|
/>
|
|
529
499
|
</div>
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
500
|
+
</div>
|
|
501
|
+
<p v-if="!documentFragments.length" class="text-sm text-slate-500">
|
|
502
|
+
{{ t('fragments.documents.empty') }}
|
|
503
|
+
</p>
|
|
504
|
+
|
|
505
|
+
<div class="rounded-md border border-slate-800 p-3">
|
|
506
|
+
<p class="mb-2 text-sm font-medium">{{ t('fragments.documents.linkTitle') }}</p>
|
|
507
|
+
<div v-if="docLinkDisabled" class="text-sm text-slate-500">
|
|
508
|
+
{{ t('fragments.documents.disabledHint') }}
|
|
509
|
+
</div>
|
|
510
|
+
<div v-else-if="!documents.connectedSources.length" class="text-sm text-slate-500">
|
|
511
|
+
{{ t('fragments.documents.connectFirst') }}
|
|
512
|
+
</div>
|
|
513
|
+
<div v-else class="flex flex-col gap-2">
|
|
514
|
+
<div class="flex flex-wrap gap-2">
|
|
515
|
+
<UButton
|
|
516
|
+
v-for="s in documents.connectedSources"
|
|
517
|
+
:key="s.source"
|
|
518
|
+
size="xs"
|
|
519
|
+
:color="docDraft.source === s.source ? 'primary' : 'neutral'"
|
|
520
|
+
:variant="docDraft.source === s.source ? 'solid' : 'outline'"
|
|
521
|
+
@click="docDraft.source = s.source"
|
|
522
|
+
>
|
|
523
|
+
{{ s.label }}
|
|
524
|
+
</UButton>
|
|
525
|
+
</div>
|
|
526
|
+
|
|
527
|
+
<!-- GitHub: search a repo + browse to the file instead of typing the ref -->
|
|
528
|
+
<template v-if="showGithubDocPicker">
|
|
529
|
+
<GitHubRepoSearchSelect v-model="docRepoId" @update:repo="docRepo = $event" />
|
|
530
|
+
<div
|
|
531
|
+
v-if="docRepoId !== undefined"
|
|
532
|
+
class="rounded-md border border-slate-800 bg-slate-900/40 p-2"
|
|
533
|
+
>
|
|
534
|
+
<p class="mb-2 text-xs text-slate-400">
|
|
535
|
+
{{ t('fragments.documents.githubBrowseHint') }}
|
|
536
|
+
</p>
|
|
537
|
+
<RepoTreeBrowser v-model="docFilePath" :repo-github-id="docRepoId" mode="file" />
|
|
538
|
+
</div>
|
|
539
|
+
</template>
|
|
540
|
+
|
|
541
|
+
<UInput v-model="docDraft.ref" :placeholder="t('fragments.documents.refPlaceholder')" />
|
|
536
542
|
<UInput
|
|
537
|
-
v-model="
|
|
538
|
-
:placeholder="t('fragments.
|
|
539
|
-
class="flex-1"
|
|
543
|
+
v-model="docDraft.tags"
|
|
544
|
+
:placeholder="t('fragments.documents.tagsPlaceholder')"
|
|
540
545
|
/>
|
|
546
|
+
<UButton
|
|
547
|
+
icon="i-lucide-link"
|
|
548
|
+
size="sm"
|
|
549
|
+
:disabled="!docDraftValid"
|
|
550
|
+
:loading="library.loading"
|
|
551
|
+
class="self-start"
|
|
552
|
+
@click="linkDocumentFragment"
|
|
553
|
+
>
|
|
554
|
+
{{ t('fragments.documents.link') }}
|
|
555
|
+
</UButton>
|
|
541
556
|
</div>
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
557
|
+
</div>
|
|
558
|
+
</div>
|
|
559
|
+
|
|
560
|
+
<!-- Repo sources -->
|
|
561
|
+
<div v-else class="flex flex-col gap-3">
|
|
562
|
+
<div
|
|
563
|
+
v-for="s in library.sources"
|
|
564
|
+
:key="s.id"
|
|
565
|
+
class="flex items-center gap-2 rounded-md border border-slate-800 bg-slate-900/60 p-3"
|
|
566
|
+
>
|
|
567
|
+
<UIcon name="i-lucide-git-branch" class="h-4 w-4 text-slate-400" />
|
|
568
|
+
<div class="min-w-0">
|
|
569
|
+
<span class="font-mono text-sm text-slate-100">
|
|
570
|
+
{{ s.repoOwner }}/{{ s.repoName
|
|
571
|
+
}}<span class="text-slate-500">/{{ s.dirPath || '' }}</span>
|
|
572
|
+
</span>
|
|
573
|
+
<p class="text-xs text-slate-500">
|
|
574
|
+
{{
|
|
575
|
+
s.lastSyncedAt
|
|
576
|
+
? t('fragments.sources.metaSynced', { ref: s.gitRef })
|
|
577
|
+
: t('fragments.sources.metaNever', { ref: s.gitRef })
|
|
578
|
+
}}
|
|
579
|
+
</p>
|
|
580
|
+
</div>
|
|
581
|
+
<UBadge
|
|
582
|
+
v-if="library.sourceChanges[s.id]"
|
|
583
|
+
size="xs"
|
|
584
|
+
color="warning"
|
|
585
|
+
variant="subtle"
|
|
586
|
+
class="ms-auto"
|
|
549
587
|
>
|
|
550
|
-
{{
|
|
551
|
-
|
|
588
|
+
{{
|
|
589
|
+
t(
|
|
590
|
+
'fragments.sources.changes',
|
|
591
|
+
{ count: library.sourceChanges[s.id] },
|
|
592
|
+
library.sourceChanges[s.id] ?? 0,
|
|
593
|
+
)
|
|
594
|
+
}}
|
|
595
|
+
</UBadge>
|
|
596
|
+
<div class="ms-auto flex gap-1">
|
|
597
|
+
<UButton
|
|
598
|
+
icon="i-lucide-search-check"
|
|
599
|
+
size="xs"
|
|
600
|
+
variant="ghost"
|
|
601
|
+
@click="checkSource(s.id)"
|
|
602
|
+
/>
|
|
603
|
+
<UButton
|
|
604
|
+
icon="i-lucide-refresh-cw"
|
|
605
|
+
size="xs"
|
|
606
|
+
variant="ghost"
|
|
607
|
+
:loading="library.loading"
|
|
608
|
+
@click="syncSource(s.id)"
|
|
609
|
+
/>
|
|
610
|
+
<UButton
|
|
611
|
+
icon="i-lucide-unplug"
|
|
612
|
+
size="xs"
|
|
613
|
+
color="error"
|
|
614
|
+
variant="ghost"
|
|
615
|
+
@click="unlinkSource(s.id)"
|
|
616
|
+
/>
|
|
617
|
+
</div>
|
|
618
|
+
</div>
|
|
619
|
+
<p v-if="!library.sources.length" class="text-sm text-slate-500">
|
|
620
|
+
{{ t('fragments.sources.empty') }}
|
|
621
|
+
</p>
|
|
622
|
+
|
|
623
|
+
<div class="rounded-md border border-slate-800 p-3">
|
|
624
|
+
<p class="mb-2 text-sm font-medium">{{ t('fragments.sources.linkTitle') }}</p>
|
|
625
|
+
<div class="flex flex-col gap-2">
|
|
626
|
+
<!-- Connected: search a repo + browse to the guideline directory -->
|
|
627
|
+
<template v-if="githubReady">
|
|
628
|
+
<GitHubRepoSearchSelect v-model="sourceRepoId" @update:repo="sourceRepo = $event" />
|
|
629
|
+
<div
|
|
630
|
+
v-if="sourceRepoId !== undefined"
|
|
631
|
+
class="rounded-md border border-slate-800 bg-slate-900/40 p-2"
|
|
632
|
+
>
|
|
633
|
+
<p class="mb-2 text-xs text-slate-400">
|
|
634
|
+
{{ t('fragments.sources.browseHint') }}
|
|
635
|
+
</p>
|
|
636
|
+
<RepoTreeBrowser v-model="sourceDir" :repo-github-id="sourceRepoId" mode="dir" />
|
|
637
|
+
<p class="mt-2 truncate text-xs text-slate-400">
|
|
638
|
+
<template v-if="sourceDir">
|
|
639
|
+
{{ t('fragments.sources.selectedDir') }}
|
|
640
|
+
<code class="text-slate-200">{{ sourceDir }}</code>
|
|
641
|
+
</template>
|
|
642
|
+
<template v-else>{{ t('fragments.sources.wholeRepo') }}</template>
|
|
643
|
+
</p>
|
|
644
|
+
</div>
|
|
645
|
+
</template>
|
|
646
|
+
|
|
647
|
+
<!-- Not connected: manual owner/name/dir fallback -->
|
|
648
|
+
<template v-else>
|
|
649
|
+
<div class="flex gap-2">
|
|
650
|
+
<UInput
|
|
651
|
+
v-model="manualSource.repoOwner"
|
|
652
|
+
:placeholder="t('fragments.sources.ownerPlaceholder')"
|
|
653
|
+
class="flex-1"
|
|
654
|
+
/>
|
|
655
|
+
<UInput
|
|
656
|
+
v-model="manualSource.repoName"
|
|
657
|
+
:placeholder="t('fragments.sources.repoPlaceholder')"
|
|
658
|
+
class="flex-1"
|
|
659
|
+
/>
|
|
660
|
+
</div>
|
|
661
|
+
<UInput
|
|
662
|
+
v-model="manualSource.dirPath"
|
|
663
|
+
:placeholder="t('fragments.sources.dirPlaceholder')"
|
|
664
|
+
/>
|
|
665
|
+
</template>
|
|
666
|
+
|
|
667
|
+
<UInput v-model="sourceRef" :placeholder="t('fragments.sources.refPlaceholder')" />
|
|
668
|
+
<UButton
|
|
669
|
+
icon="i-lucide-link"
|
|
670
|
+
size="sm"
|
|
671
|
+
:disabled="!sourceValid"
|
|
672
|
+
:loading="library.loading"
|
|
673
|
+
class="self-start"
|
|
674
|
+
@click="linkSource"
|
|
675
|
+
>
|
|
676
|
+
{{ t('fragments.sources.link') }}
|
|
677
|
+
</UButton>
|
|
678
|
+
</div>
|
|
552
679
|
</div>
|
|
553
680
|
</div>
|
|
554
|
-
</
|
|
681
|
+
</template>
|
|
555
682
|
</div>
|
|
556
683
|
</template>
|