@cat-factory/app 0.74.2 → 0.75.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.
@@ -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 { DocumentSourceKind, FragmentOwnerKind, ResolvedFragment } from '~/types/domain'
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
- const sourceDraft = ref({ repoOwner: '', repoName: '', dirPath: '', gitRef: '' })
182
- const sourceValid = computed(
183
- () => sourceDraft.value.repoOwner.trim() && sourceDraft.value.repoName.trim(),
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
- if (!sourceValid.value) return
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: sourceDraft.value.repoOwner.trim(),
191
- repoName: sourceDraft.value.repoName.trim(),
192
- dirPath: sourceDraft.value.dirPath.trim() || undefined,
193
- gitRef: sourceDraft.value.gitRef.trim() || undefined,
268
+ repoOwner: ownerName.owner,
269
+ repoName: ownerName.name,
270
+ dirPath,
271
+ gitRef: sourceRef.value.trim() || undefined,
194
272
  })
195
- sourceDraft.value = { repoOwner: '', repoName: '', dirPath: '', gitRef: '' }
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
- <p class="text-sm text-slate-400">
246
- <template v-if="isWorkspace">
247
- {{ t('fragments.intro.workspace') }}
248
- </template>
249
- <template v-else>
250
- {{ t('fragments.intro.account') }}
251
- </template>
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
- <!-- Resolved (merged) catalog — workspace scope only -->
268
- <div v-if="tab === 'catalog'" class="flex flex-col gap-2">
269
- <p class="text-xs text-slate-500">
270
- {{
271
- t(
272
- 'fragments.catalog.summary',
273
- { count: library.resolved.length, builtin: library.builtinCount },
274
- library.resolved.length,
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
- <!-- Hand-authored (this tier) -->
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
- icon="i-lucide-trash-2"
326
- size="xs"
327
- color="error"
328
- variant="ghost"
329
- class="ms-auto"
330
- @click="removeFragment(f.id)"
331
- />
332
- </div>
333
- <p v-if="!library.fragments.length" class="text-sm text-slate-500">
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
- <!-- Document-backed (living) fragments -->
370
- <div v-else-if="tab === 'documents'" class="flex flex-col gap-3">
371
- <p class="text-xs text-slate-500">
372
- {{ t('fragments.documents.intro') }}
373
- </p>
374
-
375
- <div
376
- v-for="f in documentFragments"
377
- :key="f.id"
378
- class="flex items-start gap-2 rounded-md border border-slate-800 bg-slate-900/60 p-3"
379
- >
380
- <UIcon name="i-lucide-radio" class="mt-0.5 h-4 w-4 text-emerald-400" />
381
- <div class="min-w-0">
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="success" variant="subtle">
385
- {{ f.documentRef?.source }}
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
- <div class="ms-auto flex gap-1">
394
- <UButton
395
- icon="i-lucide-refresh-cw"
396
- size="xs"
397
- variant="ghost"
398
- :loading="library.loading"
399
- :title="t('fragments.documents.refreshTitle')"
400
- @click="refreshFragment(f.id)"
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
- </div>
411
- <p v-if="!documentFragments.length" class="text-sm text-slate-500">
412
- {{ t('fragments.documents.empty') }}
413
- </p>
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
- <div class="rounded-md border border-slate-800 p-3">
416
- <p class="mb-2 text-sm font-medium">{{ t('fragments.documents.linkTitle') }}</p>
417
- <div v-if="docLinkDisabled" class="text-sm text-slate-500">
418
- {{ t('fragments.documents.disabledHint') }}
419
- </div>
420
- <div v-else-if="!documents.connectedSources.length" class="text-sm text-slate-500">
421
- {{ t('fragments.documents.connectFirst') }}
422
- </div>
423
- <div v-else class="flex flex-col gap-2">
424
- <div class="flex flex-wrap gap-2">
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
- v-for="s in documents.connectedSources"
427
- :key="s.source"
428
- size="xs"
429
- :color="docDraft.source === s.source ? 'primary' : 'neutral'"
430
- :variant="docDraft.source === s.source ? 'solid' : 'outline'"
431
- @click="docDraft.source = s.source"
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
- {{ s.label }}
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
- <!-- Repo sources -->
453
- <div v-else class="flex flex-col gap-3">
454
- <div
455
- v-for="s in library.sources"
456
- :key="s.id"
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
- <div class="rounded-md border border-slate-800 p-3">
516
- <p class="mb-2 text-sm font-medium">{{ t('fragments.sources.linkTitle') }}</p>
517
- <div class="flex flex-col gap-2">
518
- <div class="flex gap-2">
519
- <UInput
520
- v-model="sourceDraft.repoOwner"
521
- :placeholder="t('fragments.sources.ownerPlaceholder')"
522
- class="flex-1"
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
- <UInput
525
- v-model="sourceDraft.repoName"
526
- :placeholder="t('fragments.sources.repoPlaceholder')"
527
- class="flex-1"
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
- <div class="flex gap-2">
531
- <UInput
532
- v-model="sourceDraft.dirPath"
533
- :placeholder="t('fragments.sources.dirPlaceholder')"
534
- class="flex-1"
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="sourceDraft.gitRef"
538
- :placeholder="t('fragments.sources.refPlaceholder')"
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
- <UButton
543
- icon="i-lucide-link"
544
- size="sm"
545
- :disabled="!sourceValid"
546
- :loading="library.loading"
547
- class="self-start"
548
- @click="linkSource"
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
- {{ t('fragments.sources.link') }}
551
- </UButton>
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
- </div>
681
+ </template>
555
682
  </div>
556
683
  </template>
@@ -0,0 +1,118 @@
1
+ <script setup lang="ts">
2
+ // A reusable server-side GitHub repository picker: the same searchable combobox the
3
+ // add-service modal uses, extracted so any window that needs to pick a repo the App
4
+ // can access gets identical behaviour (type ≥ MIN_SEARCH_LEN chars → the backend
5
+ // filters `owner/name`, nothing is prefetched). Exposes the selected repo's numeric
6
+ // id via `v-model`, and emits the full `GitHubAvailableRepo` (owner/name/flags) via
7
+ // `update:repo` for callers that need more than the id.
8
+ import { refDebounced } from '@vueuse/core'
9
+ import type { GitHubAvailableRepo } from '~/types/domain'
10
+
11
+ const props = defineProps<{
12
+ /** Selected repo GitHub numeric id, via v-model. */
13
+ modelValue?: number
14
+ }>()
15
+ const emit = defineEmits<{
16
+ 'update:modelValue': [number | undefined]
17
+ 'update:repo': [GitHubAvailableRepo | undefined]
18
+ }>()
19
+
20
+ const { t } = useI18n()
21
+ const github = useGitHubStore()
22
+
23
+ // A wide App install (or a PAT) can expose hundreds of repos — too many to prefetch and
24
+ // filter client-side — so the picker searches SERVER-SIDE once the user types at least
25
+ // MIN_SEARCH_LEN characters (debounced). Below the gate the list stays empty.
26
+ const MIN_SEARCH_LEN = 3
27
+ const repoSearch = ref('')
28
+ const repoSearchDebounced = refDebounced(repoSearch, 250)
29
+ const repoQueryRaw = computed(() => repoSearchDebounced.value.trim())
30
+ const belowMinChars = computed(() => repoQueryRaw.value.length < MIN_SEARCH_LEN)
31
+
32
+ // The picked repo, captured when selected — the loaded list is volatile (a later search
33
+ // replaces it), so the selection can't be derived from `availableRepos` after the fact.
34
+ const selectedRepo = ref<GitHubAvailableRepo | undefined>(undefined)
35
+
36
+ function toRepoItem(r: GitHubAvailableRepo) {
37
+ const suffix = r.private ? t('github.addService.repoLabel.private') : ''
38
+ return { label: `${r.owner}/${r.name}${suffix}`, value: r.githubId }
39
+ }
40
+
41
+ const repoItems = computed(() => github.availableRepos.map(toRepoItem))
42
+ const queryMatches = computed(() => (belowMinChars.value ? [] : repoItems.value))
43
+
44
+ // Items fed to the combobox: the matches plus the current selection kept present, so the
45
+ // menu still renders the selected repo's label after a later search replaces the list.
46
+ const repoMenuItems = computed(() => {
47
+ const matches = queryMatches.value
48
+ if (props.modelValue === undefined) return matches
49
+ if (matches.some((r) => r.value === props.modelValue)) return matches
50
+ return selectedRepo.value ? [toRepoItem(selectedRepo.value), ...matches] : matches
51
+ })
52
+
53
+ // Fetch matches server-side as the debounced query changes; below the gate clear the list.
54
+ watch(repoQueryRaw, (q) => {
55
+ void github.loadAvailableRepos(q.length >= MIN_SEARCH_LEN ? q : '')
56
+ })
57
+
58
+ const selectedId = computed({
59
+ get: () => props.modelValue,
60
+ set: (v: number | undefined) => emit('update:modelValue', v),
61
+ })
62
+
63
+ // On selection, capture the picked repo (from the still-current loaded list) and surface it.
64
+ watch(
65
+ () => props.modelValue,
66
+ (id) => {
67
+ if (id === undefined) {
68
+ selectedRepo.value = undefined
69
+ emit('update:repo', undefined)
70
+ return
71
+ }
72
+ const found = github.availableRepos.find((r) => r.githubId === id)
73
+ if (found) {
74
+ selectedRepo.value = found
75
+ emit('update:repo', found)
76
+ }
77
+ },
78
+ )
79
+
80
+ function clear() {
81
+ emit('update:modelValue', undefined)
82
+ emit('update:repo', undefined)
83
+ repoSearch.value = ''
84
+ }
85
+ </script>
86
+
87
+ <template>
88
+ <UInputMenu
89
+ v-model="selectedId"
90
+ v-model:search-term="repoSearch"
91
+ :items="repoMenuItems"
92
+ :ignore-filter="true"
93
+ value-key="value"
94
+ :loading="github.loadingAvailable"
95
+ icon="i-lucide-search"
96
+ :placeholder="t('github.addService.searchPlaceholder')"
97
+ class="w-full"
98
+ >
99
+ <template v-if="selectedId !== undefined" #trailing>
100
+ <UButton
101
+ color="neutral"
102
+ variant="link"
103
+ size="sm"
104
+ icon="i-lucide-x"
105
+ :aria-label="t('github.addService.clearSelection')"
106
+ @click.stop="clear"
107
+ />
108
+ </template>
109
+ <template #empty>
110
+ <span v-if="belowMinChars">
111
+ {{ t('github.addService.searchMinChars', { min: MIN_SEARCH_LEN }, MIN_SEARCH_LEN) }}
112
+ </span>
113
+ <span v-else-if="!github.loadingAvailable">
114
+ {{ t('github.addService.noMatches', { query: repoQueryRaw }) }}
115
+ </span>
116
+ </template>
117
+ </UInputMenu>
118
+ </template>
@@ -66,6 +66,26 @@ const results = ref<TaskSearchResult[]>([])
66
66
  const searching = ref(false)
67
67
  const searchError = ref<string | null>(null)
68
68
 
69
+ // Already-imported issues, scoped to the target container's repo on the backend
70
+ // (GitHub narrows to the service's linked repo, exactly as search does; repo-less
71
+ // sources are unaffected). Held locally rather than read from the shared workspace
72
+ // list, so a task created for one service never offers issues from sibling repos.
73
+ const imported = ref<SourceTask[]>([])
74
+ async function reloadImported() {
75
+ try {
76
+ imported.value = await tasks.listTasksForBlock(props.scopeBlockId)
77
+ } catch {
78
+ imported.value = []
79
+ }
80
+ }
81
+ // Re-scope when the target container changes (its repo, hence the in-repo issues, differ).
82
+ watch(
83
+ () => props.scopeBlockId,
84
+ () => {
85
+ reloadImported()
86
+ },
87
+ )
88
+
69
89
  // Debounced search: free text hits the tracker; a query that's clearly a URL/key
70
90
  // is left to the explicit "by reference" row below (search won't surface it).
71
91
  // Re-scope when `scopeBlockId` changes too (a GitHub search is scoped to the block's
@@ -108,7 +128,7 @@ function keyFor(externalId: string): string {
108
128
  const importedRows = computed(() => {
109
129
  if (!source.value) return []
110
130
  const q = query.value.trim().toLowerCase()
111
- return tasks.tasks
131
+ return imported.value
112
132
  .filter((t) => t.source === source.value)
113
133
  .filter((t) => !chosen.value.has(keyFor(t.externalId)))
114
134
  .filter(
@@ -120,7 +140,7 @@ const importedRows = computed(() => {
120
140
  const searchRows = computed(() => {
121
141
  if (!source.value) return []
122
142
  const importedIds = new Set(
123
- tasks.tasks.filter((t) => t.source === source.value).map((t) => t.externalId),
143
+ imported.value.filter((t) => t.source === source.value).map((t) => t.externalId),
124
144
  )
125
145
  return results.value
126
146
  .filter((r) => !importedIds.has(r.externalId))
@@ -195,8 +215,8 @@ function pickRef(q: string) {
195
215
  }
196
216
 
197
217
  onMounted(() => {
198
- // Keep the quick-pick list current (cheap; the store dedupes).
199
- tasks.loadTasks().catch(() => {})
218
+ // Load the quick-pick list, scoped to the target container's repo.
219
+ reloadImported()
200
220
  })
201
221
  </script>
202
222
 
@@ -62,7 +62,11 @@ export function tasksApi({ send, ws }: ApiContext) {
62
62
  checkTaskSource: (workspaceId: string, source: TaskSourceKind) =>
63
63
  send(diagnoseTaskSourceContract, { pathPrefix: ws(workspaceId), pathParams: { source } }),
64
64
 
65
- listTasks: (workspaceId: string) => send(listTasksContract, { pathPrefix: ws(workspaceId) }),
65
+ // `blockId` scopes the listed issues to that block's service repo for a
66
+ // repo-backed source (GitHub Issues), exactly as search does; omitted → the
67
+ // whole workspace.
68
+ listTasks: (workspaceId: string, blockId?: string) =>
69
+ send(listTasksContract, { pathPrefix: ws(workspaceId), queryParams: { blockId } }),
66
70
 
67
71
  importTask: (workspaceId: string, source: TaskSourceKind, body: { ref: string }) =>
68
72
  send(importTaskContract, { pathPrefix: ws(workspaceId), pathParams: { source }, body }),
@@ -119,6 +119,18 @@ export const useTasksStore = defineStore('tasks', () => {
119
119
  tasks.value = await api.listTasks(workspace.requireId())
120
120
  }
121
121
 
122
+ /**
123
+ * Fetch imported issues scoped to a block's service repo (GitHub only — a
124
+ * repo-backed source narrows to that service's linked repo, exactly as `search`
125
+ * does; repo-less sources are unaffected). Returns the list WITHOUT touching the
126
+ * shared `tasks` state, so a repo-scoped view (the issue picker) can hold its own
127
+ * list without narrowing the workspace-wide one other views rely on. Omit
128
+ * `blockId` for the whole workspace.
129
+ */
130
+ async function listTasksForBlock(blockId?: string): Promise<SourceTask[]> {
131
+ return api.listTasks(workspace.requireId(), blockId)
132
+ }
133
+
122
134
  /** Import (fetch + persist) an issue by key or URL from a source. */
123
135
  async function importTask(source: TaskSourceKind, ref: string): Promise<SourceTask> {
124
136
  loading.value = true
@@ -218,6 +230,7 @@ export const useTasksStore = defineStore('tasks', () => {
218
230
  disconnect,
219
231
  setEnabled,
220
232
  loadTasks,
233
+ listTasksForBlock,
221
234
  importTask,
222
235
  search,
223
236
  linkToBlock,
@@ -3717,7 +3717,8 @@
3717
3717
  "connectFirst": "Connect a document source (Confluence, Notion or GitHub) under Integrations first.",
3718
3718
  "refPlaceholder": "Page id or URL (e.g. a Confluence/Notion page or GitHub file URL)",
3719
3719
  "tagsPlaceholder": "Tags, comma-separated (optional)",
3720
- "link": "Link as living fragment"
3720
+ "link": "Link as living fragment",
3721
+ "githubBrowseHint": "Browse the repo and pick the file to link."
3721
3722
  },
3722
3723
  "sources": {
3723
3724
  "metaSynced": "synced · ref {ref}",
@@ -3729,7 +3730,10 @@
3729
3730
  "repoPlaceholder": "repo",
3730
3731
  "dirPlaceholder": "dir path (e.g. guidelines)",
3731
3732
  "refPlaceholder": "ref (default HEAD)",
3732
- "link": "Link & sync"
3733
+ "link": "Link & sync",
3734
+ "browseHint": "Browse the repo and pick the directory of Markdown guidelines (or link the whole repo).",
3735
+ "selectedDir": "Directory:",
3736
+ "wholeRepo": "Whole repository (root)."
3733
3737
  },
3734
3738
  "toast": {
3735
3739
  "added": "Fragment added",
@@ -3753,7 +3757,8 @@
3753
3757
  "confirmRemove": {
3754
3758
  "title": "Delete this fragment?",
3755
3759
  "body": "\"{name}\" will be removed. This can't be undone."
3756
- }
3760
+ },
3761
+ "unavailable": "The prompt-fragment library isn't enabled for this deployment."
3757
3762
  },
3758
3763
  "sandbox": {
3759
3764
  "title": "Sandbox: prompt and model testing",
@@ -3585,7 +3585,8 @@
3585
3585
  "connectFirst": "Conecta primero una fuente de documentos (Confluence, Notion o GitHub) en Integraciones.",
3586
3586
  "refPlaceholder": "Id o URL de página (p. ej., una página de Confluence/Notion o la URL de un archivo de GitHub)",
3587
3587
  "tagsPlaceholder": "Etiquetas, separadas por comas (opcional)",
3588
- "link": "Vincular como fragmento vivo"
3588
+ "link": "Vincular como fragmento vivo",
3589
+ "githubBrowseHint": "Explora el repositorio y elige el archivo a enlazar."
3589
3590
  },
3590
3591
  "sources": {
3591
3592
  "metaSynced": "sincronizado · ref {ref}",
@@ -3597,7 +3598,10 @@
3597
3598
  "repoPlaceholder": "repositorio",
3598
3599
  "dirPlaceholder": "ruta del directorio (p. ej., guidelines)",
3599
3600
  "refPlaceholder": "ref (HEAD por defecto)",
3600
- "link": "Vincular y sincronizar"
3601
+ "link": "Vincular y sincronizar",
3602
+ "browseHint": "Explora el repositorio y elige el directorio de guías Markdown (o enlaza todo el repositorio).",
3603
+ "selectedDir": "Directorio:",
3604
+ "wholeRepo": "Repositorio completo (raíz)."
3601
3605
  },
3602
3606
  "toast": {
3603
3607
  "added": "Fragmento añadido",
@@ -3621,7 +3625,8 @@
3621
3625
  "confirmRemove": {
3622
3626
  "title": "¿Eliminar este fragmento?",
3623
3627
  "body": "Se eliminará \"{name}\". Esta acción no se puede deshacer."
3624
- }
3628
+ },
3629
+ "unavailable": "La biblioteca de fragmentos de prompt no está habilitada en esta implementación."
3625
3630
  },
3626
3631
  "sandbox": {
3627
3632
  "title": "Sandbox: pruebas de prompts y modelos",
@@ -3585,7 +3585,8 @@
3585
3585
  "connectFirst": "Connectez d'abord une source de documents (Confluence, Notion ou GitHub) dans Intégrations.",
3586
3586
  "refPlaceholder": "Id ou URL de page (par ex. une page Confluence/Notion ou l'URL d'un fichier GitHub)",
3587
3587
  "tagsPlaceholder": "Étiquettes, séparées par des virgules (facultatif)",
3588
- "link": "Lier comme fragment vivant"
3588
+ "link": "Lier comme fragment vivant",
3589
+ "githubBrowseHint": "Parcourez le dépôt et choisissez le fichier à lier."
3589
3590
  },
3590
3591
  "sources": {
3591
3592
  "metaSynced": "synchronisé · ref {ref}",
@@ -3597,7 +3598,10 @@
3597
3598
  "repoPlaceholder": "dépôt",
3598
3599
  "dirPlaceholder": "chemin du répertoire (par ex. guidelines)",
3599
3600
  "refPlaceholder": "ref (HEAD par défaut)",
3600
- "link": "Lier et synchroniser"
3601
+ "link": "Lier et synchroniser",
3602
+ "browseHint": "Parcourez le dépôt et choisissez le répertoire de consignes Markdown (ou liez tout le dépôt).",
3603
+ "selectedDir": "Répertoire :",
3604
+ "wholeRepo": "Dépôt entier (racine)."
3601
3605
  },
3602
3606
  "toast": {
3603
3607
  "added": "Fragment ajouté",
@@ -3621,7 +3625,8 @@
3621
3625
  "confirmRemove": {
3622
3626
  "title": "Supprimer ce fragment ?",
3623
3627
  "body": "\"{name}\" sera supprimé. Cette action est irréversible."
3624
- }
3628
+ },
3629
+ "unavailable": "La bibliothèque de fragments de prompt n'est pas activée pour ce déploiement."
3625
3630
  },
3626
3631
  "sandbox": {
3627
3632
  "title": "Bac à sable : test de prompts et de modèles",
@@ -3596,7 +3596,8 @@
3596
3596
  "connectFirst": "חבר תחילה מקור מסמכים (Confluence, Notion או GitHub) תחת אינטגרציות.",
3597
3597
  "refPlaceholder": "מזהה עמוד או כתובת (למשל עמוד Confluence/Notion או כתובת קובץ GitHub)",
3598
3598
  "tagsPlaceholder": "תגיות, מופרדות בפסיקים (אופציונלי)",
3599
- "link": "קשר כמקטע חי"
3599
+ "link": "קשר כמקטע חי",
3600
+ "githubBrowseHint": "עיין במאגר ובחר את הקובץ לקישור."
3600
3601
  },
3601
3602
  "sources": {
3602
3603
  "metaSynced": "סונכרן · ref {ref}",
@@ -3608,7 +3609,10 @@
3608
3609
  "repoPlaceholder": "מאגר",
3609
3610
  "dirPlaceholder": "נתיב תיקייה (למשל guidelines)",
3610
3611
  "refPlaceholder": "ref (ברירת מחדל HEAD)",
3611
- "link": "קשר וסנכרן"
3612
+ "link": "קשר וסנכרן",
3613
+ "browseHint": "עיין במאגר ובחר את תיקיית הנחיות ה-Markdown (או קשר את המאגר כולו).",
3614
+ "selectedDir": "תיקייה:",
3615
+ "wholeRepo": "המאגר כולו (השורש)."
3612
3616
  },
3613
3617
  "toast": {
3614
3618
  "added": "המקטע נוסף",
@@ -3632,7 +3636,8 @@
3632
3636
  "confirmRemove": {
3633
3637
  "title": "למחוק את המקטע הזה?",
3634
3638
  "body": "\"{name}\" יימחק. לא ניתן לבטל פעולה זו."
3635
- }
3639
+ },
3640
+ "unavailable": "ספריית מקטעי הפרומפט אינה מופעלת בפריסה זו."
3636
3641
  },
3637
3642
  "sandbox": {
3638
3643
  "title": "Sandbox: בדיקת פרומפטים ומודלים",
@@ -3598,7 +3598,8 @@
3598
3598
  "connectFirst": "まず Integrations でドキュメントソース (Confluence、Notion または GitHub) を接続してください。",
3599
3599
  "refPlaceholder": "ページ id または URL (例: Confluence/Notion ページや GitHub ファイル URL)",
3600
3600
  "tagsPlaceholder": "タグ、カンマ区切り (任意)",
3601
- "link": "リビングフラグメントとしてリンク"
3601
+ "link": "リビングフラグメントとしてリンク",
3602
+ "githubBrowseHint": "リポジトリを参照してリンクするファイルを選択します。"
3602
3603
  },
3603
3604
  "sources": {
3604
3605
  "metaSynced": "同期済み · ref {ref}",
@@ -3610,7 +3611,10 @@
3610
3611
  "repoPlaceholder": "repo",
3611
3612
  "dirPlaceholder": "ディレクトリパス (例: guidelines)",
3612
3613
  "refPlaceholder": "ref (デフォルト HEAD)",
3613
- "link": "リンクして同期"
3614
+ "link": "リンクして同期",
3615
+ "browseHint": "リポジトリを参照して Markdown ガイドラインのディレクトリを選択します(またはリポジトリ全体をリンク)。",
3616
+ "selectedDir": "ディレクトリ:",
3617
+ "wholeRepo": "リポジトリ全体(ルート)。"
3614
3618
  },
3615
3619
  "toast": {
3616
3620
  "added": "フラグメントを追加しました",
@@ -3634,7 +3638,8 @@
3634
3638
  "confirmRemove": {
3635
3639
  "title": "このフラグメントを削除しますか?",
3636
3640
  "body": "「{name}」が削除されます。 この操作は取り消せません。"
3637
- }
3641
+ },
3642
+ "unavailable": "このデプロイではプロンプトフラグメントライブラリが有効になっていません。"
3638
3643
  },
3639
3644
  "sandbox": {
3640
3645
  "title": "Sandbox: プロンプトとモデルのテスト",
@@ -3585,7 +3585,8 @@
3585
3585
  "connectFirst": "Najpierw połącz źródło dokumentów (Confluence, Notion lub GitHub) w sekcji Integracje.",
3586
3586
  "refPlaceholder": "Id lub URL strony (np. strona Confluence/Notion lub URL pliku GitHub)",
3587
3587
  "tagsPlaceholder": "Tagi, oddzielone przecinkami (opcjonalnie)",
3588
- "link": "Połącz jako żywy fragment"
3588
+ "link": "Połącz jako żywy fragment",
3589
+ "githubBrowseHint": "Przeglądaj repozytorium i wybierz plik do połączenia."
3589
3590
  },
3590
3591
  "sources": {
3591
3592
  "metaSynced": "zsynchronizowano · ref {ref}",
@@ -3597,7 +3598,10 @@
3597
3598
  "repoPlaceholder": "repozytorium",
3598
3599
  "dirPlaceholder": "ścieżka katalogu (np. guidelines)",
3599
3600
  "refPlaceholder": "ref (domyślnie HEAD)",
3600
- "link": "Połącz i synchronizuj"
3601
+ "link": "Połącz i synchronizuj",
3602
+ "browseHint": "Przeglądaj repozytorium i wybierz katalog wytycznych Markdown (lub połącz całe repozytorium).",
3603
+ "selectedDir": "Katalog:",
3604
+ "wholeRepo": "Całe repozytorium (katalog główny)."
3601
3605
  },
3602
3606
  "toast": {
3603
3607
  "added": "Dodano fragment",
@@ -3621,7 +3625,8 @@
3621
3625
  "confirmRemove": {
3622
3626
  "title": "Usunąć ten fragment?",
3623
3627
  "body": "\"{name}\" zostanie usunięty. Tej operacji nie można cofnąć."
3624
- }
3628
+ },
3629
+ "unavailable": "Biblioteka fragmentów promptów nie jest włączona w tym wdrożeniu."
3625
3630
  },
3626
3631
  "sandbox": {
3627
3632
  "title": "Piaskownica: testowanie promptów i modeli",
@@ -3598,7 +3598,8 @@
3598
3598
  "connectFirst": "Önce Entegrasyonlar altında bir belge kaynağı (Confluence, Notion veya GitHub) bağlayın.",
3599
3599
  "refPlaceholder": "Sayfa kimliği veya URL (örn. bir Confluence/Notion sayfası veya GitHub dosya URL'si)",
3600
3600
  "tagsPlaceholder": "Etiketler, virgülle ayrılmış (isteğe bağlı)",
3601
- "link": "Canlı parça olarak bağla"
3601
+ "link": "Canlı parça olarak bağla",
3602
+ "githubBrowseHint": "Depoya göz atın ve bağlanacak dosyayı seçin."
3602
3603
  },
3603
3604
  "sources": {
3604
3605
  "metaSynced": "eşitlendi · ref {ref}",
@@ -3610,7 +3611,10 @@
3610
3611
  "repoPlaceholder": "repo",
3611
3612
  "dirPlaceholder": "dizin yolu (örn. guidelines)",
3612
3613
  "refPlaceholder": "ref (varsayılan HEAD)",
3613
- "link": "Bağla ve eşitle"
3614
+ "link": "Bağla ve eşitle",
3615
+ "browseHint": "Depoya göz atın ve Markdown yönerge dizinini seçin (veya tüm depoyu bağlayın).",
3616
+ "selectedDir": "Dizin:",
3617
+ "wholeRepo": "Tüm depo (kök)."
3614
3618
  },
3615
3619
  "toast": {
3616
3620
  "added": "Parça eklendi",
@@ -3634,7 +3638,8 @@
3634
3638
  "confirmRemove": {
3635
3639
  "title": "Bu parça silinsin mi?",
3636
3640
  "body": "\"{name}\" kaldırılacak. Bu işlem geri alınamaz."
3637
- }
3641
+ },
3642
+ "unavailable": "Bu dağıtımda istem parçası kütüphanesi etkin değil."
3638
3643
  },
3639
3644
  "sandbox": {
3640
3645
  "title": "Sandbox: prompt ve model testi",
@@ -3585,7 +3585,8 @@
3585
3585
  "connectFirst": "Спершу під'єднайте джерело документів (Confluence, Notion або GitHub) у розділі Інтеграції.",
3586
3586
  "refPlaceholder": "Id або URL сторінки (напр. сторінка Confluence/Notion чи URL файлу GitHub)",
3587
3587
  "tagsPlaceholder": "Теги, розділені комами (необов'язково)",
3588
- "link": "Прив'язати як живий фрагмент"
3588
+ "link": "Прив'язати як живий фрагмент",
3589
+ "githubBrowseHint": "Перегляньте репозиторій і виберіть файл для зв’язування."
3589
3590
  },
3590
3591
  "sources": {
3591
3592
  "metaSynced": "синхронізовано · ref {ref}",
@@ -3597,7 +3598,10 @@
3597
3598
  "repoPlaceholder": "репозиторій",
3598
3599
  "dirPlaceholder": "шлях до каталогу (напр. guidelines)",
3599
3600
  "refPlaceholder": "ref (типово HEAD)",
3600
- "link": "Прив'язати й синхронізувати"
3601
+ "link": "Прив'язати й синхронізувати",
3602
+ "browseHint": "Перегляньте репозиторій і виберіть каталог настанов Markdown (або зв’яжіть увесь репозиторій).",
3603
+ "selectedDir": "Каталог:",
3604
+ "wholeRepo": "Увесь репозиторій (корінь)."
3601
3605
  },
3602
3606
  "toast": {
3603
3607
  "added": "Фрагмент додано",
@@ -3621,7 +3625,8 @@
3621
3625
  "confirmRemove": {
3622
3626
  "title": "Видалити цей фрагмент?",
3623
3627
  "body": "\"{name}\" буде видалено. Цю дію не можна скасувати."
3624
- }
3628
+ },
3629
+ "unavailable": "Бібліотека фрагментів промптів не ввімкнена для цього розгортання."
3625
3630
  },
3626
3631
  "sandbox": {
3627
3632
  "title": "Пісочниця: тестування промптів і моделей",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cat-factory/app",
3
- "version": "0.74.2",
3
+ "version": "0.75.0",
4
4
  "description": "Reusable Nuxt layer for the Agent Architecture Board SPA (components, stores, composables, pages). Consume it from a thin deployment app via `extends: ['@cat-factory/app']` and point it at your backend with NUXT_PUBLIC_API_BASE. See deploy/frontend for an example.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -34,7 +34,7 @@
34
34
  "valibot": "^1.4.2",
35
35
  "vue": "^3.5.39",
36
36
  "wretch": "^3.0.9",
37
- "@cat-factory/contracts": "0.81.1"
37
+ "@cat-factory/contracts": "0.81.2"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@toad-contracts/testing": "0.3.2",