@cat-factory/app 0.199.0 → 0.200.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.
Files changed (39) hide show
  1. package/app/components/foundational/FoundationalContractSummary.vue +36 -0
  2. package/app/components/foundational/FoundationalServiceCatalogList.vue +170 -0
  3. package/app/components/foundational/FoundationalServiceManager.vue +111 -0
  4. package/app/components/foundational/FoundationalServicePanel.vue +37 -0
  5. package/app/components/foundational/FoundationalServiceRegistry.vue +339 -0
  6. package/app/components/foundational/FoundationalServiceSources.vue +398 -0
  7. package/app/components/foundational/FoundationalSuppressions.vue +75 -0
  8. package/app/components/layout/AccountFoundationalSettings.vue +25 -0
  9. package/app/components/layout/AccountSkillSettings.vue +1 -1
  10. package/app/components/settings/AccountSettingsPanel.vue +17 -1
  11. package/app/components/skills/SkillLibraryManager.vue +1 -1
  12. package/app/composables/api/foundationalServices.ts +131 -0
  13. package/app/composables/api/skills.ts +1 -1
  14. package/app/composables/useApi.ts +2 -0
  15. package/app/composables/useNavContributions.ts +1 -0
  16. package/app/composables/usePipelineErrorToast.ts +8 -0
  17. package/app/modular/nav-contributions.spec.ts +6 -0
  18. package/app/modular/nav-contributions.ts +26 -0
  19. package/app/pages/index.vue +4 -0
  20. package/app/stores/foundationalServices.spec.ts +121 -0
  21. package/app/stores/foundationalServices.ts +276 -0
  22. package/app/stores/skillLibrary.ts +1 -1
  23. package/app/stores/skills.spec.ts +1 -1
  24. package/app/stores/skills.ts +1 -1
  25. package/app/stores/ui/modals.ts +12 -0
  26. package/app/types/domain.ts +1 -0
  27. package/app/types/foundationalServices.ts +32 -0
  28. package/app/types/skills.ts +1 -1
  29. package/i18n/locales/de.json +146 -6
  30. package/i18n/locales/en.json +146 -6
  31. package/i18n/locales/es.json +146 -6
  32. package/i18n/locales/fr.json +146 -6
  33. package/i18n/locales/he.json +146 -6
  34. package/i18n/locales/it.json +146 -6
  35. package/i18n/locales/ja.json +146 -6
  36. package/i18n/locales/pl.json +146 -6
  37. package/i18n/locales/tr.json +146 -6
  38. package/i18n/locales/uk.json +146 -6
  39. package/package.json +2 -2
@@ -0,0 +1,398 @@
1
+ <script setup lang="ts">
2
+ // Repo sources of foundational-service definitions (backend/docs/adr/0031-foundational-services.md).
3
+ // Two link shapes, and the choice is the whole reason this form has a mode switch:
4
+ // - `directory` — every immediate subdirectory of the linked path is a service, identified by
5
+ // its `service.md`, with its contract files beside it. The "we keep our specs in a repo" case.
6
+ // - `files` — an explicit list of contract files, all describing the ONE service the link
7
+ // names. The "just point at my openapi.yaml" case, where there is no directory convention to
8
+ // adopt — which is why the link itself must supply the identity, and the form demands it.
9
+ // Mirrors the skill library's sources UI: with the GitHub App connected the user searches a repo
10
+ // and browses to a path; otherwise the manual owner/name fields are the fallback.
11
+ import { computed, reactive, ref, watch } from 'vue'
12
+ import type {
13
+ FoundationalServiceOwnerKind,
14
+ FoundationalServiceSourceMode,
15
+ GitHubAvailableRepo,
16
+ } from '~/types/domain'
17
+ import {
18
+ useFoundationalServices,
19
+ useFoundationalServicesStore,
20
+ } from '~/stores/foundationalServices'
21
+ import GitHubRepoSearchSelect from '~/components/github/GitHubRepoSearchSelect.vue'
22
+ import RepoTreeBrowser from '~/components/github/RepoTreeBrowser.vue'
23
+
24
+ const props = defineProps<{ kind: FoundationalServiceOwnerKind; ownerId: string }>()
25
+
26
+ const catalog =
27
+ props.kind === 'workspace'
28
+ ? useFoundationalServicesStore()
29
+ : useFoundationalServices(props.kind, props.ownerId)
30
+ const github = useGitHubStore()
31
+ const toast = useToast()
32
+ const { t, d } = useI18n()
33
+ const { confirm } = useConfirm()
34
+
35
+ // The rich GitHub pickers reuse the active board's App installation; without it the form falls
36
+ // back to manual text entry.
37
+ const githubReady = computed(() => github.available === true && github.connected)
38
+
39
+ function notifyError(title: string, e: unknown) {
40
+ toast.add({
41
+ title,
42
+ description: e instanceof Error ? e.message : String(e),
43
+ icon: 'i-lucide-triangle-alert',
44
+ color: 'error',
45
+ })
46
+ }
47
+
48
+ // Per-row in-flight tracking so only the control that triggered an action spins.
49
+ const busyRows = reactive(new Set<string>())
50
+ const rowBusy = (key: string) => busyRows.has(key)
51
+ async function withRow(key: string, fn: () => Promise<void>) {
52
+ if (busyRows.has(key)) return
53
+ busyRows.add(key)
54
+ try {
55
+ await fn()
56
+ } finally {
57
+ busyRows.delete(key)
58
+ }
59
+ }
60
+
61
+ // ---- link a repo source ----------------------------------------------------
62
+ const mode = ref<FoundationalServiceSourceMode>('directory')
63
+ const repoId = ref<number | undefined>(undefined)
64
+ const repo = ref<GitHubAvailableRepo | undefined>(undefined)
65
+ const dirPath = ref<string | undefined>(undefined)
66
+ const filePaths = ref<string[]>([])
67
+ const gitRef = ref('')
68
+ const manual = reactive({ repoOwner: '', repoName: '', dirPath: '', filePaths: '' })
69
+ const named = reactive({ serviceId: '', serviceName: '', serviceSummary: '' })
70
+ const linking = ref(false)
71
+
72
+ // A new repo selection clears whatever was browsed against the previous one.
73
+ watch(repoId, () => {
74
+ dirPath.value = undefined
75
+ filePaths.value = []
76
+ })
77
+
78
+ const modeItems = computed(() => [
79
+ { value: 'directory' as const, label: t('foundational.sources.mode.directory') },
80
+ { value: 'files' as const, label: t('foundational.sources.mode.files') },
81
+ ])
82
+
83
+ const ownerName = computed<{ owner: string; name: string } | null>(() => {
84
+ if (githubReady.value) {
85
+ return repo.value ? { owner: repo.value.owner, name: repo.value.name } : null
86
+ }
87
+ const owner = manual.repoOwner.trim()
88
+ const name = manual.repoName.trim()
89
+ return owner && name ? { owner, name } : null
90
+ })
91
+
92
+ /** The linked files, from the browser cart or the manual newline/comma list. */
93
+ const linkedFiles = computed(() =>
94
+ githubReady.value
95
+ ? filePaths.value
96
+ : manual.filePaths
97
+ .split(/[\n,]/)
98
+ .map((p) => p.trim())
99
+ .filter(Boolean),
100
+ )
101
+
102
+ // A `files` source names the service its files describe — the backend refuses the link
103
+ // otherwise, so the button is disabled rather than letting the user discover it as a 422.
104
+ const valid = computed(() => {
105
+ if (!ownerName.value) return false
106
+ if (mode.value !== 'files') return true
107
+ return Boolean(named.serviceId.trim() && named.serviceName.trim() && linkedFiles.value.length)
108
+ })
109
+
110
+ function resetDraft() {
111
+ repoId.value = undefined
112
+ repo.value = undefined
113
+ dirPath.value = undefined
114
+ filePaths.value = []
115
+ gitRef.value = ''
116
+ Object.assign(manual, { repoOwner: '', repoName: '', dirPath: '', filePaths: '' })
117
+ Object.assign(named, { serviceId: '', serviceName: '', serviceSummary: '' })
118
+ }
119
+
120
+ function toggleFile(path: string) {
121
+ filePaths.value = filePaths.value.includes(path)
122
+ ? filePaths.value.filter((p) => p !== path)
123
+ : [...filePaths.value, path]
124
+ }
125
+
126
+ async function link() {
127
+ const target = ownerName.value
128
+ if (!target || !valid.value) return
129
+ linking.value = true
130
+ try {
131
+ const result = await catalog.linkSource({
132
+ repoOwner: target.owner,
133
+ repoName: target.name,
134
+ gitRef: gitRef.value.trim() || undefined,
135
+ mode: mode.value,
136
+ dirPath: (githubReady.value ? dirPath.value : manual.dirPath.trim()) || undefined,
137
+ ...(mode.value === 'files'
138
+ ? {
139
+ filePaths: linkedFiles.value,
140
+ serviceId: named.serviceId.trim(),
141
+ serviceName: named.serviceName.trim(),
142
+ serviceSummary: named.serviceSummary.trim() || undefined,
143
+ }
144
+ : {}),
145
+ })
146
+ resetDraft()
147
+ toast.add({ title: t('foundational.toast.sourceLinked'), icon: 'i-lucide-git-branch' })
148
+ return result
149
+ } catch (e) {
150
+ notifyError(t('foundational.toast.linkSourceFailed'), e)
151
+ } finally {
152
+ linking.value = false
153
+ }
154
+ }
155
+
156
+ async function sync(id: string) {
157
+ await withRow(`sync:${id}`, async () => {
158
+ try {
159
+ const result = await catalog.syncSource(id)
160
+ toast.add({
161
+ title: t('foundational.toast.synced', {
162
+ updated: result.upserted,
163
+ removed: result.tombstoned,
164
+ }),
165
+ icon: 'i-lucide-refresh-cw',
166
+ color: 'info',
167
+ })
168
+ } catch (e) {
169
+ notifyError(t('foundational.toast.syncFailed'), e)
170
+ }
171
+ })
172
+ }
173
+
174
+ async function check(id: string) {
175
+ await withRow(`check:${id}`, async () => {
176
+ try {
177
+ const status = await catalog.checkSource(id)
178
+ toast.add({
179
+ title: status.changed
180
+ ? t('foundational.toast.changesAvailable')
181
+ : t('foundational.toast.upToDate'),
182
+ icon: status.changed ? 'i-lucide-bell-dot' : 'i-lucide-check',
183
+ })
184
+ } catch (e) {
185
+ notifyError(t('foundational.toast.checkSourceFailed'), e)
186
+ }
187
+ })
188
+ }
189
+
190
+ async function unlink(id: string) {
191
+ const source = catalog.sources.find((s) => s.id === id)
192
+ const ok = await confirm({
193
+ title: t('foundational.confirmUnlinkSource.title'),
194
+ description: t('foundational.confirmUnlinkSource.body', {
195
+ repo: source ? `${source.repoOwner}/${source.repoName}` : '',
196
+ }),
197
+ variant: 'destructive',
198
+ confirmLabel: t('foundational.confirmUnlinkSource.confirm'),
199
+ icon: 'i-lucide-unplug',
200
+ })
201
+ if (!ok) return
202
+ await withRow(`unlink:${id}`, async () => {
203
+ try {
204
+ await catalog.unlinkSource(id)
205
+ toast.add({ title: t('foundational.toast.sourceUnlinked'), icon: 'i-lucide-unplug' })
206
+ } catch (e) {
207
+ notifyError(t('foundational.toast.unlinkSourceFailed'), e)
208
+ }
209
+ })
210
+ }
211
+ </script>
212
+
213
+ <template>
214
+ <div class="flex flex-col gap-3" data-testid="foundational-sources">
215
+ <div
216
+ v-for="s in catalog.sources"
217
+ :key="s.id"
218
+ class="flex items-center gap-2 rounded-md border border-slate-800 bg-slate-900/60 p-3"
219
+ >
220
+ <UIcon name="i-lucide-git-branch" class="h-4 w-4 shrink-0 text-slate-400" />
221
+ <div class="min-w-0">
222
+ <span class="font-mono text-sm text-slate-100">
223
+ {{ s.repoOwner }}/{{ s.repoName }}<span class="text-slate-500">/{{ s.dirPath }}</span>
224
+ </span>
225
+ <p class="text-xs text-slate-500">
226
+ {{
227
+ s.mode === 'files'
228
+ ? t('foundational.sources.metaFiles', {
229
+ service: s.serviceName ?? s.serviceId ?? '',
230
+ count: s.filePaths.length,
231
+ })
232
+ : t('foundational.sources.metaDirectory')
233
+ }}
234
+ </p>
235
+ <p class="text-xs text-slate-500">
236
+ {{
237
+ s.lastSyncedAt
238
+ ? t('foundational.sources.metaSynced', {
239
+ ref: s.gitRef,
240
+ date: d(new Date(s.lastSyncedAt), 'short'),
241
+ })
242
+ : t('foundational.sources.metaNever', { ref: s.gitRef })
243
+ }}
244
+ </p>
245
+ </div>
246
+ <UBadge
247
+ v-if="catalog.sourceChanges[s.id]"
248
+ size="xs"
249
+ color="warning"
250
+ variant="subtle"
251
+ class="ms-auto"
252
+ >
253
+ {{ t('foundational.sources.changes') }}
254
+ </UBadge>
255
+ <div class="ms-auto flex gap-1">
256
+ <UButton
257
+ icon="i-lucide-search-check"
258
+ size="xs"
259
+ variant="ghost"
260
+ :loading="rowBusy(`check:${s.id}`)"
261
+ :title="t('foundational.sources.check')"
262
+ @click="check(s.id)"
263
+ />
264
+ <UButton
265
+ icon="i-lucide-refresh-cw"
266
+ size="xs"
267
+ variant="ghost"
268
+ :loading="rowBusy(`sync:${s.id}`)"
269
+ :title="t('foundational.sources.sync')"
270
+ @click="sync(s.id)"
271
+ />
272
+ <UButton
273
+ icon="i-lucide-unplug"
274
+ size="xs"
275
+ color="error"
276
+ variant="ghost"
277
+ :loading="rowBusy(`unlink:${s.id}`)"
278
+ :title="t('foundational.sources.unlink')"
279
+ @click="unlink(s.id)"
280
+ />
281
+ </div>
282
+ </div>
283
+ <p v-if="!catalog.sources.length" class="text-sm text-slate-500">
284
+ {{ t('foundational.sources.empty') }}
285
+ </p>
286
+
287
+ <!-- Linking needs the GitHub integration; say so rather than offering a form that 503s. -->
288
+ <div
289
+ v-if="!catalog.sourcesAvailable"
290
+ class="rounded-md border border-slate-800 bg-slate-900/40 p-3 text-sm text-slate-500"
291
+ >
292
+ {{ t('foundational.sources.githubRequired') }}
293
+ </div>
294
+ <div v-else class="rounded-md border border-slate-800 p-3">
295
+ <p class="mb-2 text-sm font-medium">{{ t('foundational.sources.linkTitle') }}</p>
296
+ <div class="flex flex-col gap-2">
297
+ <URadioGroup v-model="mode" :items="modeItems" orientation="horizontal" size="sm" />
298
+ <p class="text-xs text-slate-500">
299
+ {{
300
+ mode === 'files'
301
+ ? t('foundational.sources.mode.filesHint')
302
+ : t('foundational.sources.mode.directoryHint')
303
+ }}
304
+ </p>
305
+
306
+ <!-- Connected: search a repo, then browse to the folder / pick the contract files -->
307
+ <template v-if="githubReady">
308
+ <GitHubRepoSearchSelect v-model="repoId" @update:repo="repo = $event" />
309
+ <div
310
+ v-if="repoId !== undefined"
311
+ class="rounded-md border border-slate-800 bg-slate-900/40 p-2"
312
+ >
313
+ <RepoTreeBrowser
314
+ v-if="mode === 'files'"
315
+ :repo-github-id="repoId"
316
+ mode="file"
317
+ multiple
318
+ :selected-paths="filePaths"
319
+ @toggle="toggleFile"
320
+ />
321
+ <RepoTreeBrowser v-else v-model="dirPath" :repo-github-id="repoId" mode="dir" />
322
+ <p class="mt-2 truncate text-xs text-slate-400">
323
+ <template v-if="mode === 'files'">
324
+ {{ t('foundational.sources.selectedFiles', { count: filePaths.length }) }}
325
+ </template>
326
+ <template v-else-if="dirPath">
327
+ {{ t('foundational.sources.selectedDir') }}
328
+ <code class="text-slate-200">{{ dirPath }}</code>
329
+ </template>
330
+ <template v-else>{{ t('foundational.sources.wholeRepo') }}</template>
331
+ </p>
332
+ </div>
333
+ </template>
334
+
335
+ <!-- Not connected to the App: manual owner/name + path fallback -->
336
+ <template v-else>
337
+ <div class="flex gap-2">
338
+ <UInput
339
+ v-model="manual.repoOwner"
340
+ :placeholder="t('foundational.sources.ownerPlaceholder')"
341
+ class="flex-1"
342
+ />
343
+ <UInput
344
+ v-model="manual.repoName"
345
+ :placeholder="t('foundational.sources.repoPlaceholder')"
346
+ class="flex-1"
347
+ />
348
+ </div>
349
+ <UTextarea
350
+ v-if="mode === 'files'"
351
+ v-model="manual.filePaths"
352
+ :rows="3"
353
+ :placeholder="t('foundational.sources.filePathsPlaceholder')"
354
+ />
355
+ <UInput
356
+ v-else
357
+ v-model="manual.dirPath"
358
+ :placeholder="t('foundational.sources.dirPlaceholder')"
359
+ />
360
+ </template>
361
+
362
+ <!-- `files` mode carries no directory convention to read identity from, so the link
363
+ supplies it. -->
364
+ <template v-if="mode === 'files'">
365
+ <div class="flex gap-2">
366
+ <UInput
367
+ v-model="named.serviceId"
368
+ :placeholder="t('foundational.sources.serviceIdPlaceholder')"
369
+ class="flex-1"
370
+ />
371
+ <UInput
372
+ v-model="named.serviceName"
373
+ :placeholder="t('foundational.sources.serviceNamePlaceholder')"
374
+ class="flex-1"
375
+ />
376
+ </div>
377
+ <UInput
378
+ v-model="named.serviceSummary"
379
+ :placeholder="t('foundational.sources.serviceSummaryPlaceholder')"
380
+ />
381
+ </template>
382
+
383
+ <UInput v-model="gitRef" :placeholder="t('foundational.sources.refPlaceholder')" />
384
+ <UButton
385
+ icon="i-lucide-link"
386
+ size="sm"
387
+ :disabled="!valid"
388
+ :loading="linking"
389
+ class="self-start"
390
+ data-testid="foundational-link-source"
391
+ @click="link"
392
+ >
393
+ {{ t('foundational.sources.link') }}
394
+ </UButton>
395
+ </div>
396
+ </div>
397
+ </div>
398
+ </template>
@@ -0,0 +1,75 @@
1
+ <script setup lang="ts">
2
+ // What this board is opted OUT of (backend/docs/adr/0031-foundational-services.md). A suppressed id is
3
+ // by construction absent from the merged catalog, so without this list the suppress action would
4
+ // be a one-way door — which is why it is a section of its own rather than a badge on the catalog.
5
+ //
6
+ // `inherited: false` is rendered distinctly rather than hidden: the tombstone shadows nothing
7
+ // today (the account withdrew the service, or this row is what remains of the board deleting its
8
+ // own registration), and an operator reading it as "a capability is being withheld" would go
9
+ // looking for something that is not there.
10
+ import { reactive } from 'vue'
11
+ import { useFoundationalServicesStore } from '~/stores/foundationalServices'
12
+
13
+ const catalog = useFoundationalServicesStore()
14
+ const toast = useToast()
15
+ const { t } = useI18n()
16
+
17
+ const busyRows = reactive(new Set<string>())
18
+ const rowBusy = (id: string) => busyRows.has(id)
19
+
20
+ async function restore(serviceId: string) {
21
+ if (busyRows.has(serviceId)) return
22
+ busyRows.add(serviceId)
23
+ try {
24
+ await catalog.restore(serviceId)
25
+ toast.add({ title: t('foundational.toast.restored'), icon: 'i-lucide-eye' })
26
+ } catch (e) {
27
+ toast.add({
28
+ title: t('foundational.toast.restoreFailed'),
29
+ description: e instanceof Error ? e.message : String(e),
30
+ icon: 'i-lucide-triangle-alert',
31
+ color: 'error',
32
+ })
33
+ } finally {
34
+ busyRows.delete(serviceId)
35
+ }
36
+ }
37
+ </script>
38
+
39
+ <template>
40
+ <div
41
+ v-if="catalog.suppressions.length"
42
+ class="flex flex-col gap-2"
43
+ data-testid="foundational-suppressions"
44
+ >
45
+ <p class="text-sm font-medium">{{ t('foundational.suppressions.title') }}</p>
46
+ <p class="text-xs text-slate-500">{{ t('foundational.suppressions.intro') }}</p>
47
+ <div
48
+ v-for="s in catalog.suppressions"
49
+ :key="s.id"
50
+ class="flex items-center gap-2 rounded-md border border-slate-800 bg-slate-900/40 p-3"
51
+ >
52
+ <UIcon name="i-lucide-eye-off" class="h-4 w-4 shrink-0 text-slate-500" />
53
+ <div class="min-w-0 flex-1">
54
+ <p class="truncate text-sm text-slate-300">
55
+ {{ s.name || s.id }}
56
+ <code v-if="s.name" class="ms-1 text-[11px] text-slate-500">{{ s.id }}</code>
57
+ </p>
58
+ <p v-if="s.summary" class="text-xs text-slate-500">{{ s.summary }}</p>
59
+ <p v-if="!s.inherited" class="text-[11px] text-slate-500">
60
+ {{ t('foundational.suppressions.shadowsNothing') }}
61
+ </p>
62
+ </div>
63
+ <UButton
64
+ icon="i-lucide-eye"
65
+ size="xs"
66
+ variant="ghost"
67
+ :loading="rowBusy(s.id)"
68
+ :data-testid="`foundational-restore-${s.id}`"
69
+ @click="restore(s.id)"
70
+ >
71
+ {{ t('foundational.suppressions.restore') }}
72
+ </UButton>
73
+ </div>
74
+ </div>
75
+ </template>
@@ -0,0 +1,25 @@
1
+ <script setup lang="ts">
2
+ // Account-tier foundational services (backend/docs/adr/0031-foundational-services.md): the shared
3
+ // capabilities the whole organisation runs — file storage, notifications, audit — which every
4
+ // board in the account inherits and can override or opt out of. A body-only section rendered in
5
+ // the "Foundational services" tab of AccountSettingsPanel; available for ALL account types.
6
+ import FoundationalServiceManager from '~/components/foundational/FoundationalServiceManager.vue'
7
+
8
+ const props = defineProps<{ accountId: string }>()
9
+ const { t } = useI18n()
10
+ </script>
11
+
12
+ <template>
13
+ <div class="space-y-6 text-sm">
14
+ <section>
15
+ <p class="mb-3 text-[11px] text-slate-400">
16
+ {{ t('layout.accountFoundational.intro') }}
17
+ </p>
18
+ <FoundationalServiceManager
19
+ kind="account"
20
+ :owner-id="props.accountId"
21
+ :show-catalog="false"
22
+ />
23
+ </section>
24
+ </div>
25
+ </template>
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- // Account-tier repo-sourced Claude Skills library (docs/initiatives/repo-skills.md): a team
2
+ // Account-tier repo-sourced Claude Skills library (ADR 0024): a team
3
3
  // authors skills in a repo (`<skill>/SKILL.md` + resources), the account syncs them into a
4
4
  // catalog shared across its workspaces, and a pipeline `skill` step runs one. A body-only
5
5
  // section rendered in the "Skills" tab of AccountSettingsPanel; available for ALL account types.
@@ -2,12 +2,14 @@
2
2
  // Account settings — a single tabbed modal for the per-account configuration, distinct
3
3
  // from Workspace settings. Hosts the team panel (members + roles, invitations, email
4
4
  // sender, account-wide API keys; org-scoped, with a create-org CTA on a personal account)
5
- // and the account-tier prompt-fragment library (available for every account type).
5
+ // the account-tier prompt-fragment library, the repo-sourced skills, and the foundational-service
6
+ // catalog (all available for every account type).
6
7
  // Opened from the SideBar Configuration section, the account switcher and the command
7
8
  // bar; bound to the `ui` store so any surface can open it and deep-link to a tab.
8
9
  import AccountTeamSettings from '~/components/layout/AccountTeamSettings.vue'
9
10
  import AccountFragmentSettings from '~/components/layout/AccountFragmentSettings.vue'
10
11
  import AccountSkillSettings from '~/components/layout/AccountSkillSettings.vue'
12
+ import AccountFoundationalSettings from '~/components/layout/AccountFoundationalSettings.vue'
11
13
 
12
14
  const { t } = useI18n()
13
15
  const ui = useUiStore()
@@ -39,6 +41,12 @@ const tabs = computed(() => [
39
41
  icon: 'i-lucide-book-open-check',
40
42
  slot: 'skills',
41
43
  },
44
+ {
45
+ value: 'foundational',
46
+ label: t('settings.account.tabs.foundational'),
47
+ icon: 'i-lucide-boxes',
48
+ slot: 'foundational',
49
+ },
42
50
  ])
43
51
  </script>
44
52
 
@@ -61,6 +69,14 @@ const tabs = computed(() => [
61
69
  <template #fragments>
62
70
  <AccountFragmentSettings :account-id="accounts.activeAccountId" />
63
71
  </template>
72
+ <template #foundational>
73
+ <!-- Key on the account for the same reason the skills tab is: a mid-modal account
74
+ switch must remount against a fresh owner-keyed store, not the stale initial one. -->
75
+ <AccountFoundationalSettings
76
+ :key="accounts.activeAccountId ?? undefined"
77
+ :account-id="accounts.activeAccountId"
78
+ />
79
+ </template>
64
80
  <template #skills>
65
81
  <!-- Key on the account so a mid-modal account switch remounts against a fresh
66
82
  account-keyed skill-library store rather than the stale initial one. -->
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- // Repo-sourced Claude Skills library manager (docs/initiatives/repo-skills.md), for the account
2
+ // Repo-sourced Claude Skills library manager (ADR 0024), for the account
3
3
  // tier (skills are a single tier — shared across the account's workspaces). Link repo directories
4
4
  // of `<skill>/SKILL.md` folders, resync them (with a "changes available" badge), and review the
5
5
  // synced skill catalog a pipeline `skill` step picks from. Mirrors the fragment library's