@cat-factory/app 0.115.2 → 0.115.3

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.
@@ -6,7 +6,7 @@
6
6
  // the merged catalog (built-in ∪ account ∪ workspace) an agent is selected from per
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
- import { computed, ref, watch } from 'vue'
9
+ import { computed, reactive, ref, watch } from 'vue'
10
10
  import type {
11
11
  DocumentSourceKind,
12
12
  FragmentOwnerKind,
@@ -111,6 +111,24 @@ function notifyError(title: string, e: unknown) {
111
111
  })
112
112
  }
113
113
 
114
+ // Per-row / per-form in-flight tracking. The store's single `library.loading` flag
115
+ // drove every row's button at once (UX-29) and cross-spun the add/link forms; key
116
+ // each async action so only the control that triggered it shows a spinner.
117
+ const busyRows = reactive(new Set<string>())
118
+ const rowBusy = (key: string) => busyRows.has(key)
119
+ async function withRow(key: string, fn: () => Promise<void>) {
120
+ if (busyRows.has(key)) return
121
+ busyRows.add(key)
122
+ try {
123
+ await fn()
124
+ } finally {
125
+ busyRows.delete(key)
126
+ }
127
+ }
128
+ const creating = ref(false)
129
+ const linkingDoc = ref(false)
130
+ const linkingSource = ref(false)
131
+
114
132
  // ---- create a hand-authored fragment --------------------------------------
115
133
  const draft = ref({ title: '', summary: '', body: '', tags: '' })
116
134
  const draftValid = computed(
@@ -119,6 +137,7 @@ const draftValid = computed(
119
137
 
120
138
  async function createFragment() {
121
139
  if (!draftValid.value) return
140
+ creating.value = true
122
141
  try {
123
142
  await library.create({
124
143
  title: draft.value.title.trim(),
@@ -133,6 +152,8 @@ async function createFragment() {
133
152
  toast.add({ title: t('fragments.toast.added'), icon: 'i-lucide-check' })
134
153
  } catch (e) {
135
154
  notifyError(t('fragments.toast.addFailed'), e)
155
+ } finally {
156
+ creating.value = false
136
157
  }
137
158
  }
138
159
 
@@ -146,12 +167,14 @@ async function removeFragment(id: string) {
146
167
  icon: 'i-lucide-trash-2',
147
168
  })
148
169
  if (!ok) return
149
- try {
150
- await library.remove(id)
151
- toast.add({ title: t('fragments.toast.removed'), icon: 'i-lucide-trash-2' })
152
- } catch (e) {
153
- notifyError(t('fragments.toast.removeFailed'), e)
154
- }
170
+ await withRow(`remove:${id}`, async () => {
171
+ try {
172
+ await library.remove(id)
173
+ toast.add({ title: t('fragments.toast.removed'), icon: 'i-lucide-trash-2' })
174
+ } catch (e) {
175
+ notifyError(t('fragments.toast.removeFailed'), e)
176
+ }
177
+ })
155
178
  }
156
179
 
157
180
  // ---- document-backed (living) fragments -----------------------------------
@@ -201,6 +224,7 @@ const documentFragments = computed(() => library.fragments.filter((f) => f.docum
201
224
 
202
225
  async function linkDocumentFragment() {
203
226
  if (!docDraftValid.value) return
227
+ linkingDoc.value = true
204
228
  try {
205
229
  await library.createDocumentFragment({
206
230
  source: docDraft.value.source as DocumentSourceKind,
@@ -214,16 +238,20 @@ async function linkDocumentFragment() {
214
238
  toast.add({ title: t('fragments.toast.documentLinked'), icon: 'i-lucide-link' })
215
239
  } catch (e) {
216
240
  notifyError(t('fragments.toast.linkDocumentFailed'), e)
241
+ } finally {
242
+ linkingDoc.value = false
217
243
  }
218
244
  }
219
245
 
220
246
  async function refreshFragment(id: string) {
221
- try {
222
- await library.refreshDocumentFragment(id)
223
- toast.add({ title: t('fragments.toast.refreshed'), icon: 'i-lucide-refresh-cw' })
224
- } catch (e) {
225
- notifyError(t('fragments.toast.refreshFailed'), e)
226
- }
247
+ await withRow(`refresh:${id}`, async () => {
248
+ try {
249
+ await library.refreshDocumentFragment(id)
250
+ toast.add({ title: t('fragments.toast.refreshed'), icon: 'i-lucide-refresh-cw' })
251
+ } catch (e) {
252
+ notifyError(t('fragments.toast.refreshFailed'), e)
253
+ }
254
+ })
227
255
  }
228
256
 
229
257
  // ---- repo sources ----------------------------------------------------------
@@ -263,6 +291,7 @@ async function linkSource() {
263
291
  if (!ownerName) return
264
292
  const dirPath =
265
293
  (githubReady.value ? sourceDir.value : manualSource.value.dirPath.trim()) || undefined
294
+ linkingSource.value = true
266
295
  try {
267
296
  const source = await library.linkSource({
268
297
  repoOwner: ownerName.owner,
@@ -271,48 +300,71 @@ async function linkSource() {
271
300
  gitRef: sourceRef.value.trim() || undefined,
272
301
  })
273
302
  resetSourceDraft()
303
+ // Auto-sync the freshly-linked source via the store method directly (not the
304
+ // `syncSource` row wrapper): a failure here should surface as a link failure, and
305
+ // the form-level `linkingSource` spinner already covers the whole operation.
274
306
  await library.syncSource(source.id)
275
307
  toast.add({ title: t('fragments.toast.sourceLinked'), icon: 'i-lucide-git-branch' })
276
308
  } catch (e) {
277
309
  notifyError(t('fragments.toast.linkSourceFailed'), e)
310
+ } finally {
311
+ linkingSource.value = false
278
312
  }
279
313
  }
280
314
 
281
315
  async function syncSource(id: string) {
282
- try {
283
- const result = await library.syncSource(id)
284
- toast.add({
285
- title: t('fragments.toast.synced', {
286
- updated: result.upserted,
287
- removed: result.tombstoned,
288
- }),
289
- icon: 'i-lucide-refresh-cw',
290
- color: 'info',
291
- })
292
- } catch (e) {
293
- notifyError(t('fragments.toast.syncFailed'), e)
294
- }
316
+ await withRow(`sync:${id}`, async () => {
317
+ try {
318
+ const result = await library.syncSource(id)
319
+ toast.add({
320
+ title: t('fragments.toast.synced', {
321
+ updated: result.upserted,
322
+ removed: result.tombstoned,
323
+ }),
324
+ icon: 'i-lucide-refresh-cw',
325
+ color: 'info',
326
+ })
327
+ } catch (e) {
328
+ notifyError(t('fragments.toast.syncFailed'), e)
329
+ }
330
+ })
295
331
  }
296
332
 
297
333
  async function checkSource(id: string) {
298
- try {
299
- const status = await library.checkSource(id)
300
- toast.add({
301
- title: status.changed ? t('fragments.toast.changesAvailable') : t('fragments.toast.upToDate'),
302
- icon: status.changed ? 'i-lucide-bell-dot' : 'i-lucide-check',
303
- })
304
- } catch (e) {
305
- notifyError(t('fragments.toast.checkSourceFailed'), e)
306
- }
334
+ await withRow(`check:${id}`, async () => {
335
+ try {
336
+ const status = await library.checkSource(id)
337
+ toast.add({
338
+ title: status.changed
339
+ ? t('fragments.toast.changesAvailable')
340
+ : t('fragments.toast.upToDate'),
341
+ icon: status.changed ? 'i-lucide-bell-dot' : 'i-lucide-check',
342
+ })
343
+ } catch (e) {
344
+ notifyError(t('fragments.toast.checkSourceFailed'), e)
345
+ }
346
+ })
307
347
  }
308
348
 
309
349
  async function unlinkSource(id: string) {
310
- try {
311
- await library.unlinkSource(id)
312
- toast.add({ title: t('fragments.toast.sourceUnlinked'), icon: 'i-lucide-unplug' })
313
- } catch (e) {
314
- notifyError(t('fragments.toast.unlinkSourceFailed'), e)
315
- }
350
+ const source = library.sources.find((s) => s.id === id)
351
+ const repo = source ? `${source.repoOwner}/${source.repoName}` : ''
352
+ const ok = await confirm({
353
+ title: t('fragments.confirmUnlinkSource.title'),
354
+ description: t('fragments.confirmUnlinkSource.body', { repo }),
355
+ variant: 'destructive',
356
+ confirmLabel: t('fragments.confirmUnlinkSource.confirm'),
357
+ icon: 'i-lucide-unplug',
358
+ })
359
+ if (!ok) return
360
+ await withRow(`unlink:${id}`, async () => {
361
+ try {
362
+ await library.unlinkSource(id)
363
+ toast.add({ title: t('fragments.toast.sourceUnlinked'), icon: 'i-lucide-unplug' })
364
+ } catch (e) {
365
+ notifyError(t('fragments.toast.unlinkSourceFailed'), e)
366
+ }
367
+ })
316
368
  }
317
369
  </script>
318
370
 
@@ -417,6 +469,7 @@ async function unlinkSource(id: string) {
417
469
  color="error"
418
470
  variant="ghost"
419
471
  class="ms-auto"
472
+ :loading="rowBusy(`remove:${f.id}`)"
420
473
  @click="removeFragment(f.id)"
421
474
  />
422
475
  </div>
@@ -446,7 +499,7 @@ async function unlinkSource(id: string) {
446
499
  icon="i-lucide-plus"
447
500
  size="sm"
448
501
  :disabled="!draftValid"
449
- :loading="library.loading"
502
+ :loading="creating"
450
503
  class="self-start"
451
504
  @click="createFragment"
452
505
  >
@@ -487,7 +540,7 @@ async function unlinkSource(id: string) {
487
540
  icon="i-lucide-refresh-cw"
488
541
  size="xs"
489
542
  variant="ghost"
490
- :loading="library.loading"
543
+ :loading="rowBusy(`refresh:${f.id}`)"
491
544
  :title="t('fragments.documents.refreshTitle')"
492
545
  @click="refreshFragment(f.id)"
493
546
  />
@@ -496,6 +549,7 @@ async function unlinkSource(id: string) {
496
549
  size="xs"
497
550
  color="error"
498
551
  variant="ghost"
552
+ :loading="rowBusy(`remove:${f.id}`)"
499
553
  @click="removeFragment(f.id)"
500
554
  />
501
555
  </div>
@@ -553,7 +607,7 @@ async function unlinkSource(id: string) {
553
607
  icon="i-lucide-link"
554
608
  size="sm"
555
609
  :disabled="!docDraftValid"
556
- :loading="library.loading"
610
+ :loading="linkingDoc"
557
611
  class="self-start"
558
612
  @click="linkDocumentFragment"
559
613
  >
@@ -598,13 +652,14 @@ async function unlinkSource(id: string) {
598
652
  icon="i-lucide-search-check"
599
653
  size="xs"
600
654
  variant="ghost"
655
+ :loading="rowBusy(`check:${s.id}`)"
601
656
  @click="checkSource(s.id)"
602
657
  />
603
658
  <UButton
604
659
  icon="i-lucide-refresh-cw"
605
660
  size="xs"
606
661
  variant="ghost"
607
- :loading="library.loading"
662
+ :loading="rowBusy(`sync:${s.id}`)"
608
663
  @click="syncSource(s.id)"
609
664
  />
610
665
  <UButton
@@ -612,6 +667,7 @@ async function unlinkSource(id: string) {
612
667
  size="xs"
613
668
  color="error"
614
669
  variant="ghost"
670
+ :loading="rowBusy(`unlink:${s.id}`)"
615
671
  @click="unlinkSource(s.id)"
616
672
  />
617
673
  </div>
@@ -669,7 +725,7 @@ async function unlinkSource(id: string) {
669
725
  icon="i-lucide-link"
670
726
  size="sm"
671
727
  :disabled="!sourceValid"
672
- :loading="library.loading"
728
+ :loading="linkingSource"
673
729
  class="self-start"
674
730
  @click="linkSource"
675
731
  >
@@ -6,7 +6,14 @@
6
6
  // - Mentions (per-account): toggle + GitHub-user-id → Slack-member-id map.
7
7
  import { computed, reactive, ref, watch } from 'vue'
8
8
  import type { NotificationType } from '~/types/notifications'
9
- import type { SlackMemberMappingEntry, SlackMemberRole, SlackRoute } from '~/types/slack'
9
+ import type { SlackMemberRole, SlackRoute } from '~/types/slack'
10
+ import {
11
+ type MemberRow,
12
+ emptyMemberRow,
13
+ hasHalfFilledRow,
14
+ toMemberEntries,
15
+ toMemberRow,
16
+ } from '~/utils/slackMemberMapping'
10
17
  import IntegrationBackTitle from '~/components/layout/IntegrationBackTitle.vue'
11
18
  import SecretInput from '~/components/common/SecretInput.vue'
12
19
 
@@ -60,9 +67,15 @@ const routes = reactive<Record<NotificationType, SlackRoute>>({
60
67
  initiative: { enabled: false, channel: '' },
61
68
  })
62
69
  const mentionsEnabled = ref(false)
63
- const mapping = ref<SlackMemberMappingEntry[]>([])
70
+ // Editable member rows carry a client-only stable `uid` (see `slackMemberMapping`) so
71
+ // a mid-list delete keys the v-model by identity, not the array index (index keys
72
+ // silently rebound a neighbour's inputs — UX-23).
73
+ let uidSeq = 0
74
+ const nextUid = () => `m${++uidSeq}`
75
+ const mapping = ref<MemberRow[]>([])
64
76
  const tokenInput = ref('')
65
77
  const busy = ref(false)
78
+ const connectingOAuth = ref(false)
66
79
 
67
80
  function notifyError(title: string, e: unknown) {
68
81
  toast.add({
@@ -84,7 +97,7 @@ watch(
84
97
  routes[type] = slack.settings?.routes[type] ?? { enabled: false, channel: '' }
85
98
  }
86
99
  mentionsEnabled.value = slack.settings?.mentionsEnabled ?? false
87
- mapping.value = slack.memberMapping.map((e) => ({ role: 'engineering', ...e }))
100
+ mapping.value = slack.memberMapping.map((e) => toMemberRow(e, nextUid()))
88
101
  } catch (e) {
89
102
  notifyError(t('slack.error.loadSettings'), e)
90
103
  }
@@ -94,9 +107,13 @@ watch(
94
107
  )
95
108
 
96
109
  async function connectViaOAuth() {
110
+ connectingOAuth.value = true
97
111
  try {
112
+ // On success the browser navigates away, so `connectingOAuth` never resets here —
113
+ // it only clears on the error path below.
98
114
  window.location.href = await slack.installUrl()
99
115
  } catch (e) {
116
+ connectingOAuth.value = false
100
117
  notifyError(t('slack.error.startOAuth'), e)
101
118
  }
102
119
  }
@@ -144,17 +161,29 @@ async function saveRouting() {
144
161
  }
145
162
 
146
163
  function addMapping() {
147
- mapping.value.push({ userId: '', slackUserId: '', role: 'engineering' })
164
+ mapping.value.push(emptyMemberRow(nextUid()))
148
165
  }
149
- function removeMapping(index: number) {
150
- mapping.value.splice(index, 1)
166
+ function removeMapping(uid: string) {
167
+ mapping.value = mapping.value.filter((e) => e.uid !== uid)
151
168
  }
152
169
  async function saveMapping() {
170
+ // A partially-filled row (one id present, the other blank) used to be silently
171
+ // dropped on save (UX-23) — block instead so the user doesn't lose the entry. A
172
+ // fully-empty row is just an unused slot and is ignored.
173
+ if (hasHalfFilledRow(mapping.value)) {
174
+ toast.add({
175
+ title: t('slack.members.incompleteTitle'),
176
+ description: t('slack.members.incompleteBody'),
177
+ icon: 'i-lucide-triangle-alert',
178
+ color: 'warning',
179
+ })
180
+ return
181
+ }
153
182
  busy.value = true
154
183
  try {
155
- const entries = mapping.value.filter((e) => e.userId.trim() && e.slackUserId.trim())
184
+ const entries = toMemberEntries(mapping.value)
156
185
  await slack.updateMemberMapping(entries)
157
- mapping.value = slack.memberMapping.map((e) => ({ ...e }))
186
+ mapping.value = slack.memberMapping.map((e) => toMemberRow(e, nextUid()))
158
187
  toast.add({ title: t('slack.toast.mapSaved'), icon: 'i-lucide-check', color: 'success' })
159
188
  } catch (e) {
160
189
  notifyError(t('slack.error.saveMap'), e)
@@ -181,6 +210,7 @@ async function saveMapping() {
181
210
  v-if="slack.oauthEnabled"
182
211
  color="primary"
183
212
  icon="i-lucide-slack"
213
+ :loading="connectingOAuth"
184
214
  @click="connectViaOAuth"
185
215
  >
186
216
  {{ t('slack.connect.addToSlack') }}
@@ -287,7 +317,7 @@ async function saveMapping() {
287
317
  </template>
288
318
  </i18n-t>
289
319
  </p>
290
- <div v-for="(entry, i) in mapping" :key="i" class="flex items-center gap-2">
320
+ <div v-for="entry in mapping" :key="entry.uid" class="flex items-center gap-2">
291
321
  <UInput
292
322
  v-model="entry.userId"
293
323
  size="sm"
@@ -312,7 +342,7 @@ async function saveMapping() {
312
342
  variant="ghost"
313
343
  size="xs"
314
344
  icon="i-lucide-trash-2"
315
- @click="removeMapping(i)"
345
+ @click="removeMapping(entry.uid)"
316
346
  />
317
347
  </div>
318
348
  <div class="flex justify-between">
@@ -0,0 +1,94 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import type { SlackMemberMappingEntry } from '~/types/slack'
3
+ import {
4
+ type MemberRow,
5
+ emptyMemberRow,
6
+ hasHalfFilledRow,
7
+ toMemberEntries,
8
+ toMemberRow,
9
+ } from './slackMemberMapping'
10
+
11
+ const row = (partial: Partial<MemberRow> & { uid: string }): MemberRow => ({
12
+ userId: '',
13
+ slackUserId: '',
14
+ role: 'engineering',
15
+ ...partial,
16
+ })
17
+
18
+ describe('hasHalfFilledRow', () => {
19
+ it('is false for fully-filled rows', () => {
20
+ expect(hasHalfFilledRow([row({ uid: 'a', userId: 'usr_1', slackUserId: 'U1' })])).toBe(false)
21
+ })
22
+
23
+ it('is false for fully-empty rows (unused slots)', () => {
24
+ expect(hasHalfFilledRow([row({ uid: 'a' }), row({ uid: 'b' })])).toBe(false)
25
+ })
26
+
27
+ it('is true when only the user id is filled', () => {
28
+ expect(hasHalfFilledRow([row({ uid: 'a', userId: 'usr_1' })])).toBe(true)
29
+ })
30
+
31
+ it('is true when only the Slack id is filled', () => {
32
+ expect(hasHalfFilledRow([row({ uid: 'a', slackUserId: 'U1' })])).toBe(true)
33
+ })
34
+
35
+ it('treats whitespace-only ids as blank', () => {
36
+ expect(hasHalfFilledRow([row({ uid: 'a', userId: ' ', slackUserId: 'U1' })])).toBe(true)
37
+ })
38
+
39
+ it('flags a half-filled row among valid ones', () => {
40
+ expect(
41
+ hasHalfFilledRow([
42
+ row({ uid: 'a', userId: 'usr_1', slackUserId: 'U1' }),
43
+ row({ uid: 'b', userId: 'usr_2' }),
44
+ ]),
45
+ ).toBe(true)
46
+ })
47
+ })
48
+
49
+ describe('toMemberEntries', () => {
50
+ it('keeps fully-filled rows, drops empty slots, and strips uid', () => {
51
+ const rows: MemberRow[] = [
52
+ row({ uid: 'a', userId: 'usr_1', slackUserId: 'U1', role: 'product' }),
53
+ row({ uid: 'b' }), // empty slot — dropped
54
+ row({ uid: 'c', userId: 'usr_2', slackUserId: 'U2' }),
55
+ ]
56
+ expect(toMemberEntries(rows)).toEqual([
57
+ { userId: 'usr_1', slackUserId: 'U1', role: 'product' },
58
+ { userId: 'usr_2', slackUserId: 'U2', role: 'engineering' },
59
+ ])
60
+ })
61
+
62
+ it('does not leak the client-only uid onto the wire payload', () => {
63
+ const [entry] = toMemberEntries([row({ uid: 'a', userId: 'usr_1', slackUserId: 'U1' })])
64
+ expect(entry).not.toHaveProperty('uid')
65
+ })
66
+ })
67
+
68
+ describe('toMemberRow', () => {
69
+ it('stamps the uid and defaults a missing role', () => {
70
+ const entry: SlackMemberMappingEntry = { userId: 'usr_1', slackUserId: 'U1' }
71
+ expect(toMemberRow(entry, 'm1')).toEqual({
72
+ userId: 'usr_1',
73
+ slackUserId: 'U1',
74
+ role: 'engineering',
75
+ uid: 'm1',
76
+ })
77
+ })
78
+
79
+ it('preserves an explicit role', () => {
80
+ const entry: SlackMemberMappingEntry = { userId: 'usr_1', slackUserId: 'U1', role: 'product' }
81
+ expect(toMemberRow(entry, 'm2').role).toBe('product')
82
+ })
83
+ })
84
+
85
+ describe('emptyMemberRow', () => {
86
+ it('builds a blank engineering row with the given uid', () => {
87
+ expect(emptyMemberRow('m9')).toEqual({
88
+ uid: 'm9',
89
+ userId: '',
90
+ slackUserId: '',
91
+ role: 'engineering',
92
+ })
93
+ })
94
+ })
@@ -0,0 +1,46 @@
1
+ import type { SlackMemberMappingEntry } from '~/types/slack'
2
+
3
+ // Pure helpers for the Slack member-mapping editor (SlackPanel.vue). Extracted so
4
+ // the save-time integrity rules (UX-23) can be unit-tested without mounting the panel.
5
+
6
+ /**
7
+ * An editable member-map row: the wire entry plus a client-only stable `uid` so a
8
+ * mid-list delete keys the `v-model` by identity, not the array index (index keys
9
+ * silently rebound a neighbour's inputs — UX-23).
10
+ */
11
+ export type MemberRow = SlackMemberMappingEntry & { uid: string }
12
+
13
+ /**
14
+ * True when any row has exactly one of the two ids filled. A half-entered mapping
15
+ * used to be silently dropped on save (UX-23); the panel blocks the save instead so
16
+ * the user doesn't lose it. A fully-empty row is an unused slot, not half-filled.
17
+ */
18
+ export function hasHalfFilledRow(
19
+ rows: readonly Pick<MemberRow, 'userId' | 'slackUserId'>[],
20
+ ): boolean {
21
+ return rows.some((e) => Boolean(e.userId.trim()) !== Boolean(e.slackUserId.trim()))
22
+ }
23
+
24
+ /**
25
+ * The rows to persist: fully-filled only (empty slots dropped), with the client-only
26
+ * `uid` stripped from the wire payload.
27
+ */
28
+ export function toMemberEntries(rows: readonly MemberRow[]): SlackMemberMappingEntry[] {
29
+ return rows
30
+ .filter((e) => e.userId.trim() && e.slackUserId.trim())
31
+ .map(({ uid: _uid, ...entry }) => entry)
32
+ }
33
+
34
+ /**
35
+ * Wrap a stored wire entry as an editable row: stamp a stable `uid` and default the
36
+ * `role` (absent on older maps) so the initial load and the post-save reload produce
37
+ * identical rows rather than drifting on the default.
38
+ */
39
+ export function toMemberRow(entry: SlackMemberMappingEntry, uid: string): MemberRow {
40
+ return { role: 'engineering', ...entry, uid }
41
+ }
42
+
43
+ /** A fresh, empty editable row stamped with the given stable `uid`. */
44
+ export function emptyMemberRow(uid: string): MemberRow {
45
+ return { uid, userId: '', slackUserId: '', role: 'engineering' }
46
+ }
@@ -3368,6 +3368,11 @@
3368
3368
  "title": "Dieses Fragment löschen?",
3369
3369
  "body": "\"{name}\" wird entfernt. Dies kann nicht rückgängig gemacht werden."
3370
3370
  },
3371
+ "confirmUnlinkSource": {
3372
+ "title": "Diese Quelle trennen?",
3373
+ "body": "„{repo}“ und die davon synchronisierten Richtlinien-Fragmente werden entfernt. Du kannst sie später erneut verknüpfen.",
3374
+ "confirm": "Trennen"
3375
+ },
3371
3376
  "unavailable": "Die Prompt-Fragment-Bibliothek ist für dieses Deployment nicht aktiviert."
3372
3377
  },
3373
3378
  "brainstorm": {
@@ -3838,7 +3843,9 @@
3838
3843
  "userIdPlaceholder": "Benutzer-ID (usr_...)",
3839
3844
  "slackIdPlaceholder": "Slack-Mitglieds-ID (U...)",
3840
3845
  "add": "Mitglied hinzufügen",
3841
- "save": "Zuordnung speichern"
3846
+ "save": "Zuordnung speichern",
3847
+ "incompleteTitle": "Unvollständige Mitgliederzeile",
3848
+ "incompleteBody": "Gib vor dem Speichern sowohl die Benutzer-ID als auch die Slack-Mitglieds-ID ein oder entferne die Zeile."
3842
3849
  },
3843
3850
  "routable": {
3844
3851
  "merge_review": "Merge-Review",
@@ -2986,7 +2986,9 @@
2986
2986
  "userIdPlaceholder": "User id (usr_...)",
2987
2987
  "slackIdPlaceholder": "Slack member id (U...)",
2988
2988
  "add": "Add member",
2989
- "save": "Save map"
2989
+ "save": "Save map",
2990
+ "incompleteTitle": "Incomplete member row",
2991
+ "incompleteBody": "Fill in both the user id and the Slack member id, or remove the row, before saving."
2990
2992
  },
2991
2993
  "routable": {
2992
2994
  "merge_review": "Merge review",
@@ -4319,6 +4321,11 @@
4319
4321
  "title": "Delete this fragment?",
4320
4322
  "body": "\"{name}\" will be removed. This can't be undone."
4321
4323
  },
4324
+ "confirmUnlinkSource": {
4325
+ "title": "Unlink this source?",
4326
+ "body": "\"{repo}\" and the guideline fragments it synced will be removed. You can re-link it later.",
4327
+ "confirm": "Unlink"
4328
+ },
4322
4329
  "unavailable": "The prompt-fragment library isn't enabled for this deployment."
4323
4330
  },
4324
4331
  "sandbox": {
@@ -2899,7 +2899,9 @@
2899
2899
  "userIdPlaceholder": "Id de usuario (usr_...)",
2900
2900
  "slackIdPlaceholder": "Id de miembro de Slack (U...)",
2901
2901
  "add": "Anadir miembro",
2902
- "save": "Guardar mapa"
2902
+ "save": "Guardar mapa",
2903
+ "incompleteTitle": "Fila de miembro incompleta",
2904
+ "incompleteBody": "Antes de guardar, completa tanto el id de usuario como el id de miembro de Slack, o elimina la fila."
2903
2905
  },
2904
2906
  "routable": {
2905
2907
  "merge_review": "Revision de fusion",
@@ -4152,6 +4154,11 @@
4152
4154
  "title": "¿Eliminar este fragmento?",
4153
4155
  "body": "Se eliminará \"{name}\". Esta acción no se puede deshacer."
4154
4156
  },
4157
+ "confirmUnlinkSource": {
4158
+ "title": "¿Desvincular esta fuente?",
4159
+ "body": "«{repo}» y los fragmentos de directrices que sincronizó se eliminarán. Puedes volver a vincularla más tarde.",
4160
+ "confirm": "Desvincular"
4161
+ },
4155
4162
  "unavailable": "La biblioteca de fragmentos de prompt no está habilitada en esta implementación."
4156
4163
  },
4157
4164
  "sandbox": {
@@ -2899,7 +2899,9 @@
2899
2899
  "userIdPlaceholder": "Id utilisateur (usr_...)",
2900
2900
  "slackIdPlaceholder": "Id de membre Slack (U...)",
2901
2901
  "add": "Ajouter un membre",
2902
- "save": "Enregistrer la carte"
2902
+ "save": "Enregistrer la carte",
2903
+ "incompleteTitle": "Ligne de membre incomplète",
2904
+ "incompleteBody": "Avant d’enregistrer, renseignez l’id utilisateur et l’id de membre Slack, ou supprimez la ligne."
2903
2905
  },
2904
2906
  "routable": {
2905
2907
  "merge_review": "Revue de fusion",
@@ -4152,6 +4154,11 @@
4152
4154
  "title": "Supprimer ce fragment ?",
4153
4155
  "body": "\"{name}\" sera supprimé. Cette action est irréversible."
4154
4156
  },
4157
+ "confirmUnlinkSource": {
4158
+ "title": "Dissocier cette source ?",
4159
+ "body": "« {repo} » et les fragments de directives qu’elle a synchronisés seront supprimés. Vous pourrez la relier plus tard.",
4160
+ "confirm": "Dissocier"
4161
+ },
4155
4162
  "unavailable": "La bibliothèque de fragments de prompt n'est pas activée pour ce déploiement."
4156
4163
  },
4157
4164
  "sandbox": {
@@ -2910,7 +2910,9 @@
2910
2910
  "userIdPlaceholder": "מזהה משתמש (usr_...)",
2911
2911
  "slackIdPlaceholder": "מזהה חבר Slack (U...)",
2912
2912
  "add": "הוסף חבר",
2913
- "save": "שמור מפה"
2913
+ "save": "שמור מפה",
2914
+ "incompleteTitle": "שורת חבר חסרה",
2915
+ "incompleteBody": "לפני השמירה מלא גם את מזהה המשתמש וגם את מזהה החבר ב-Slack, או הסר את השורה."
2914
2916
  },
2915
2917
  "routable": {
2916
2918
  "merge_review": "סקירת מיזוג",
@@ -4163,6 +4165,11 @@
4163
4165
  "title": "למחוק את המקטע הזה?",
4164
4166
  "body": "\"{name}\" יימחק. לא ניתן לבטל פעולה זו."
4165
4167
  },
4168
+ "confirmUnlinkSource": {
4169
+ "title": "לנתק את המקור הזה?",
4170
+ "body": "המקור \"{repo}\" והקטעים המנחים שסונכרנו ממנו יוסרו. ניתן לקשר אותו מחדש בהמשך.",
4171
+ "confirm": "ניתוק"
4172
+ },
4166
4173
  "unavailable": "ספריית מקטעי הפרומפט אינה מופעלת בפריסה זו."
4167
4174
  },
4168
4175
  "sandbox": {
@@ -3368,6 +3368,11 @@
3368
3368
  "title": "Eliminare questo frammento?",
3369
3369
  "body": "\"{name}\" verra rimosso. Questa operazione non puo essere annullata."
3370
3370
  },
3371
+ "confirmUnlinkSource": {
3372
+ "title": "Scollegare questa fonte?",
3373
+ "body": "«{repo}» e i frammenti di linee guida che ha sincronizzato verranno rimossi. Potrai ricollegarla in seguito.",
3374
+ "confirm": "Scollega"
3375
+ },
3371
3376
  "unavailable": "La libreria di frammenti di prompt non e abilitata per questo deployment."
3372
3377
  },
3373
3378
  "brainstorm": {
@@ -3838,7 +3843,9 @@
3838
3843
  "userIdPlaceholder": "Id utente (usr_...)",
3839
3844
  "slackIdPlaceholder": "Id membro Slack (U...)",
3840
3845
  "add": "Aggiungi membro",
3841
- "save": "Salva mappa"
3846
+ "save": "Salva mappa",
3847
+ "incompleteTitle": "Riga membro incompleta",
3848
+ "incompleteBody": "Prima di salvare, compila sia l’id utente sia l’id membro Slack, oppure rimuovi la riga."
3842
3849
  },
3843
3850
  "routable": {
3844
3851
  "merge_review": "Revisione del merge",
@@ -2911,7 +2911,9 @@
2911
2911
  "userIdPlaceholder": "ユーザーID (usr_...)",
2912
2912
  "slackIdPlaceholder": "SlackメンバーID (U...)",
2913
2913
  "add": "メンバーを追加",
2914
- "save": "マップを保存"
2914
+ "save": "マップを保存",
2915
+ "incompleteTitle": "メンバー行が未入力です",
2916
+ "incompleteBody": "保存する前に、ユーザー ID と Slack メンバー ID の両方を入力するか、行を削除してください。"
2915
2917
  },
2916
2918
  "routable": {
2917
2919
  "merge_review": "マージレビュー",
@@ -4164,6 +4166,11 @@
4164
4166
  "title": "このフラグメントを削除しますか?",
4165
4167
  "body": "「{name}」が削除されます。 この操作は取り消せません。"
4166
4168
  },
4169
+ "confirmUnlinkSource": {
4170
+ "title": "このソースのリンクを解除しますか?",
4171
+ "body": "「{repo}」と、そこから同期されたガイドライン断片が削除されます。後で再リンクできます。",
4172
+ "confirm": "リンク解除"
4173
+ },
4167
4174
  "unavailable": "このデプロイではプロンプトフラグメントライブラリが有効になっていません。"
4168
4175
  },
4169
4176
  "sandbox": {
@@ -2899,7 +2899,9 @@
2899
2899
  "userIdPlaceholder": "Id uzytkownika (usr_...)",
2900
2900
  "slackIdPlaceholder": "Id czlonka Slacka (U...)",
2901
2901
  "add": "Dodaj czlonka",
2902
- "save": "Zapisz mape"
2902
+ "save": "Zapisz mape",
2903
+ "incompleteTitle": "Niekompletny wiersz członka",
2904
+ "incompleteBody": "Przed zapisaniem uzupełnij zarówno identyfikator użytkownika, jak i identyfikator członka Slack, albo usuń wiersz."
2903
2905
  },
2904
2906
  "routable": {
2905
2907
  "merge_review": "Przeglad scalenia",
@@ -4152,6 +4154,11 @@
4152
4154
  "title": "Usunąć ten fragment?",
4153
4155
  "body": "\"{name}\" zostanie usunięty. Tej operacji nie można cofnąć."
4154
4156
  },
4157
+ "confirmUnlinkSource": {
4158
+ "title": "Odłączyć to źródło?",
4159
+ "body": "„{repo}” oraz zsynchronizowane z niego fragmenty wytycznych zostaną usunięte. Możesz połączyć je ponownie później.",
4160
+ "confirm": "Odłącz"
4161
+ },
4155
4162
  "unavailable": "Biblioteka fragmentów promptów nie jest włączona w tym wdrożeniu."
4156
4163
  },
4157
4164
  "sandbox": {
@@ -2911,7 +2911,9 @@
2911
2911
  "userIdPlaceholder": "Kullanıcı kimliği (usr_...)",
2912
2912
  "slackIdPlaceholder": "Slack üye kimliği (U...)",
2913
2913
  "add": "Üye ekle",
2914
- "save": "Eşlemeyi kaydet"
2914
+ "save": "Eşlemeyi kaydet",
2915
+ "incompleteTitle": "Eksik üye satırı",
2916
+ "incompleteBody": "Kaydetmeden önce hem kullanıcı kimliğini hem de Slack üye kimliğini girin ya da satırı kaldırın."
2915
2917
  },
2916
2918
  "routable": {
2917
2919
  "merge_review": "Birleştirme incelemesi",
@@ -4164,6 +4166,11 @@
4164
4166
  "title": "Bu parça silinsin mi?",
4165
4167
  "body": "\"{name}\" kaldırılacak. Bu işlem geri alınamaz."
4166
4168
  },
4169
+ "confirmUnlinkSource": {
4170
+ "title": "Bu kaynağın bağlantısı kaldırılsın mı?",
4171
+ "body": "\"{repo}\" ve onun eşitlediği kılavuz parçaları kaldırılacak. Daha sonra yeniden bağlayabilirsiniz.",
4172
+ "confirm": "Bağlantıyı kaldır"
4173
+ },
4167
4174
  "unavailable": "Bu dağıtımda istem parçası kütüphanesi etkin değil."
4168
4175
  },
4169
4176
  "sandbox": {
@@ -2899,7 +2899,9 @@
2899
2899
  "userIdPlaceholder": "Id користувача (usr_...)",
2900
2900
  "slackIdPlaceholder": "Id учасника Slack (U...)",
2901
2901
  "add": "Додати учасника",
2902
- "save": "Зберегти карту"
2902
+ "save": "Зберегти карту",
2903
+ "incompleteTitle": "Неповний рядок учасника",
2904
+ "incompleteBody": "Перед збереженням заповніть і ідентифікатор користувача, і ідентифікатор учасника Slack, або видаліть рядок."
2903
2905
  },
2904
2906
  "routable": {
2905
2907
  "merge_review": "Перевірка злиття",
@@ -4152,6 +4154,11 @@
4152
4154
  "title": "Видалити цей фрагмент?",
4153
4155
  "body": "\"{name}\" буде видалено. Цю дію не можна скасувати."
4154
4156
  },
4157
+ "confirmUnlinkSource": {
4158
+ "title": "Від’єднати це джерело?",
4159
+ "body": "«{repo}» та синхронізовані з нього фрагменти настанов буде видалено. Ви зможете під’єднати його знову пізніше.",
4160
+ "confirm": "Від’єднати"
4161
+ },
4155
4162
  "unavailable": "Бібліотека фрагментів промптів не ввімкнена для цього розгортання."
4156
4163
  },
4157
4164
  "sandbox": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cat-factory/app",
3
- "version": "0.115.2",
3
+ "version": "0.115.3",
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",
@@ -19,7 +19,7 @@
19
19
  },
20
20
  "dependencies": {
21
21
  "@nuxt/ui": "^4.9.0",
22
- "@nuxtjs/i18n": "^10.4.0",
22
+ "@nuxtjs/i18n": "^10.4.1",
23
23
  "@pinia/nuxt": "^0.11.3",
24
24
  "@toad-contracts/core": "0.4.0",
25
25
  "@toad-contracts/frontend-http-client": "0.3.2",
@@ -34,18 +34,18 @@
34
34
  "valibot": "^1.4.2",
35
35
  "vue": "3.5.39",
36
36
  "wretch": "^3.0.9",
37
- "@cat-factory/contracts": "0.127.0"
37
+ "@cat-factory/contracts": "0.127.1"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@toad-contracts/testing": "0.3.2",
41
41
  "@types/markdown-it": "^14.1.2",
42
42
  "happy-dom": "^20.10.6",
43
- "msw": "^2.14.6",
43
+ "msw": "^2.15.0",
44
44
  "nuxt": "^4.4.8",
45
45
  "typescript": "^6.0.3",
46
- "vitest": "^4.1.9",
46
+ "vitest": "^4.1.10",
47
47
  "vue-i18n-extract": "^2.0.7",
48
- "vue-tsc": "^3.3.6"
48
+ "vue-tsc": "^3.3.7"
49
49
  },
50
50
  "peerDependencies": {
51
51
  "nuxt": "^4.4.8"