@cat-factory/app 0.46.11 → 0.47.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.
Files changed (42) hide show
  1. package/app/components/auth/AuthGate.vue +3 -2
  2. package/app/components/auth/LoginScreen.vue +172 -32
  3. package/app/components/auth/ResetPasswordScreen.vue +16 -13
  4. package/app/components/auth/UserMenu.vue +3 -2
  5. package/app/components/layout/AccountDeploymentSettings.vue +307 -37
  6. package/app/components/layout/AccountFragmentSettings.vue +2 -3
  7. package/app/components/layout/AccountTeamSettings.vue +75 -45
  8. package/app/components/layout/AiProvidersBanner.vue +11 -11
  9. package/app/components/layout/BoardSwitcher.vue +63 -29
  10. package/app/components/layout/CommandBar.vue +68 -54
  11. package/app/components/layout/GitHubPatBanner.vue +13 -6
  12. package/app/components/layout/IntegrationBackTitle.vue +4 -1
  13. package/app/components/layout/IntegrationsHub.vue +67 -49
  14. package/app/components/layout/NotificationsInbox.vue +48 -19
  15. package/app/components/layout/PersonalSetupModal.vue +26 -16
  16. package/app/components/layout/ProviderConfigBanner.vue +14 -9
  17. package/app/components/layout/SideBar.vue +20 -18
  18. package/app/components/layout/SpendWarningBanner.vue +16 -16
  19. package/app/components/providers/ApiKeysSection.vue +27 -3
  20. package/app/components/providers/PersonalSubscriptionSection.vue +18 -4
  21. package/app/components/providers/SignInRequiredNotice.vue +19 -0
  22. package/app/components/settings/AccountSettingsPanel.vue +9 -6
  23. package/app/components/settings/IssueTrackerPanel.vue +114 -53
  24. package/app/components/settings/LocalModeSettingsPanel.vue +60 -28
  25. package/app/components/settings/LocalModelEndpointsPanel.vue +72 -27
  26. package/app/components/settings/MergeThresholdsPanel.vue +93 -45
  27. package/app/components/settings/ModelConfigurationPanel.vue +62 -36
  28. package/app/components/settings/ObservabilityConnectionPanel.vue +65 -35
  29. package/app/components/settings/OpenRouterCatalogPanel.vue +70 -40
  30. package/app/components/settings/ProviderConnectionPanel.vue +115 -61
  31. package/app/components/settings/ServiceFragmentDefaultsPanel.vue +9 -12
  32. package/app/components/settings/UserSecretsSection.vue +37 -18
  33. package/app/components/settings/WorkspaceSettingsPanel.vue +114 -62
  34. package/app/composables/api/auth.ts +7 -0
  35. package/app/stores/auth.ts +25 -1
  36. package/app/types/accountSettings.ts +4 -0
  37. package/i18n/locales/en.json +908 -0
  38. package/i18n/locales/es.json +899 -0
  39. package/i18n/locales/fr.json +899 -0
  40. package/i18n/locales/pl.json +899 -0
  41. package/i18n/locales/uk.json +899 -0
  42. package/package.json +2 -2
@@ -1,16 +1,18 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, onMounted, reactive, ref } from 'vue'
3
+ import type { ContentStorageBackend, ContentStorageConfig } from '~/types/accountSettings'
3
4
 
4
5
  // Deployment integration secrets for an account (admin only): the Slack app OAuth
5
- // credentials and the container web-search upstream keys both moved out of env into
6
- // the per-account settings store, sealed at rest. Secrets are write-only: the panel only
7
- // ever shows whether each integration is configured (the `summary`), never the values;
8
- // blank inputs leave a configured secret unchanged. Hidden when the settings store isn't
9
- // wired (no ENCRYPTION_KEY).
6
+ // credentials, the container web-search upstream keys, and the binary-artifact (screenshot)
7
+ // content-storage backend all moved out of env into the per-account settings store, sealed
8
+ // at rest. Secrets are write-only: the panel only ever shows whether each integration is
9
+ // configured (the `summary`), never the values; blank inputs leave a configured secret
10
+ // unchanged. Hidden when the settings store isn't wired (no ENCRYPTION_KEY).
10
11
  const props = defineProps<{ accountId: string }>()
11
12
 
12
13
  const store = useAccountSettingsStore()
13
14
  const toast = useToast()
15
+ const { t } = useI18n()
14
16
 
15
17
  const slack = reactive({ clientId: '', clientSecret: '', redirectUrl: '' })
16
18
  const web = reactive({ braveApiKey: '', searxngUrl: '', searxngApiKey: '' })
@@ -19,12 +21,57 @@ const savingWeb = ref(false)
19
21
 
20
22
  const summary = computed(() => store.view?.summary ?? null)
21
23
 
24
+ // ---- Content storage (binary artifacts / screenshots) --------------------
25
+ // Exhaustive enum→key map (drift guard tier 2): every backend resolves to a static literal
26
+ // `t()` key, so adding a backend without a label fails the typecheck on this Record.
27
+ const contentBackendLabels = computed<Record<ContentStorageBackend, string>>(() => ({
28
+ off: t('layout.accountDeployment.contentStorage.backends.off'),
29
+ fs: t('layout.accountDeployment.contentStorage.backends.fs'),
30
+ s3: t('layout.accountDeployment.contentStorage.backends.s3'),
31
+ r2: t('layout.accountDeployment.contentStorage.backends.r2'),
32
+ db: t('layout.accountDeployment.contentStorage.backends.db'),
33
+ }))
34
+ const storageCapability = computed(() => store.view?.contentStorageCapability ?? null)
35
+ const storageSummary = computed(() => summary.value?.contentStorage ?? null)
36
+ const backendItems = computed(() =>
37
+ (storageCapability.value?.supportedBackends ?? []).map((b) => ({
38
+ label: contentBackendLabels.value[b],
39
+ value: b,
40
+ })),
41
+ )
42
+ const csBackend = ref<ContentStorageBackend>('off')
43
+ const cs = reactive({
44
+ basePath: '',
45
+ region: '',
46
+ bucket: '',
47
+ prefix: '',
48
+ endpoint: '',
49
+ forcePathStyle: false,
50
+ accessKeyId: '',
51
+ secretAccessKey: '',
52
+ })
53
+ const savingStorage = ref(false)
54
+
55
+ function hydrateStorage() {
56
+ const cfg = store.view?.config?.contentStorage
57
+ csBackend.value = cfg?.backend ?? storageCapability.value?.defaultBackend ?? 'off'
58
+ cs.basePath = cfg?.fs?.basePath ?? ''
59
+ cs.region = cfg?.s3?.region ?? ''
60
+ cs.bucket = cfg?.s3?.bucket ?? ''
61
+ cs.prefix = cfg?.s3?.prefix ?? ''
62
+ cs.endpoint = cfg?.s3?.endpoint ?? ''
63
+ cs.forcePathStyle = cfg?.s3?.forcePathStyle ?? false
64
+ cs.accessKeyId = ''
65
+ cs.secretAccessKey = ''
66
+ }
67
+
22
68
  onMounted(async () => {
23
69
  try {
24
70
  await store.load(props.accountId)
71
+ hydrateStorage()
25
72
  } catch (e) {
26
73
  toast.add({
27
- title: 'Could not load deployment settings',
74
+ title: t('layout.accountDeployment.loadFailed'),
28
75
  description: e instanceof Error ? e.message : String(e),
29
76
  icon: 'i-lucide-triangle-alert',
30
77
  color: 'error',
@@ -32,9 +79,75 @@ onMounted(async () => {
32
79
  }
33
80
  })
34
81
 
82
+ async function saveStorage() {
83
+ const backend = csBackend.value
84
+ const config: ContentStorageConfig = { backend }
85
+ if (backend === 'fs' && cs.basePath.trim()) {
86
+ config.fs = { basePath: cs.basePath.trim() }
87
+ }
88
+ if (backend === 's3') {
89
+ if (!cs.region.trim() || !cs.bucket.trim()) {
90
+ toast.add({
91
+ title: t('layout.accountDeployment.contentStorage.regionBucketValidation'),
92
+ color: 'error',
93
+ })
94
+ return
95
+ }
96
+ config.s3 = {
97
+ region: cs.region.trim(),
98
+ bucket: cs.bucket.trim(),
99
+ ...(cs.prefix.trim() ? { prefix: cs.prefix.trim() } : {}),
100
+ ...(cs.endpoint.trim() ? { endpoint: cs.endpoint.trim() } : {}),
101
+ ...(cs.forcePathStyle ? { forcePathStyle: true } : {}),
102
+ }
103
+ }
104
+ const input: Parameters<typeof store.save>[1] = { config: { contentStorage: config } }
105
+ if (backend === 's3') {
106
+ const id = cs.accessKeyId.trim()
107
+ const key = cs.secretAccessKey.trim()
108
+ if (id && key) {
109
+ input.secrets = { s3: { accessKeyId: id, secretAccessKey: key } }
110
+ } else if (id || key) {
111
+ toast.add({
112
+ title: t('layout.accountDeployment.contentStorage.bothKeysValidation'),
113
+ color: 'error',
114
+ })
115
+ return
116
+ } else if (!storageSummary.value?.s3CredentialsConfigured) {
117
+ toast.add({
118
+ title: t('layout.accountDeployment.contentStorage.keysValidation'),
119
+ color: 'error',
120
+ })
121
+ return
122
+ }
123
+ // else: keys already stored and none re-entered → leave them unchanged.
124
+ } else {
125
+ // Switching off S3: drop any stored S3 credentials.
126
+ input.secrets = { s3: null }
127
+ }
128
+ savingStorage.value = true
129
+ try {
130
+ await store.save(props.accountId, input)
131
+ hydrateStorage()
132
+ toast.add({
133
+ title: t('layout.accountDeployment.contentStorage.saved'),
134
+ icon: 'i-lucide-check',
135
+ color: 'success',
136
+ })
137
+ } catch (e) {
138
+ toast.add({
139
+ title: t('layout.accountDeployment.contentStorage.saveFailed'),
140
+ description: e instanceof Error ? e.message : String(e),
141
+ color: 'error',
142
+ })
143
+ } finally {
144
+ savingStorage.value = false
145
+ }
146
+ }
147
+
35
148
  async function saveSlack() {
36
149
  if (!slack.clientId.trim() || !slack.clientSecret.trim() || !slack.redirectUrl.trim()) {
37
- toast.add({ title: 'Enter the client id, secret and redirect URL', color: 'error' })
150
+ toast.add({ title: t('layout.accountDeployment.slack.validation'), color: 'error' })
38
151
  return
39
152
  }
40
153
  savingSlack.value = true
@@ -51,10 +164,14 @@ async function saveSlack() {
51
164
  slack.clientId = ''
52
165
  slack.clientSecret = ''
53
166
  slack.redirectUrl = ''
54
- toast.add({ title: 'Slack OAuth saved', icon: 'i-lucide-check', color: 'success' })
167
+ toast.add({
168
+ title: t('layout.accountDeployment.slack.saved'),
169
+ icon: 'i-lucide-check',
170
+ color: 'success',
171
+ })
55
172
  } catch (e) {
56
173
  toast.add({
57
- title: 'Could not save Slack OAuth',
174
+ title: t('layout.accountDeployment.slack.saveFailed'),
58
175
  description: e instanceof Error ? e.message : String(e),
59
176
  color: 'error',
60
177
  })
@@ -67,10 +184,14 @@ async function clearSlack() {
67
184
  savingSlack.value = true
68
185
  try {
69
186
  await store.save(props.accountId, { secrets: { slackOAuth: null } })
70
- toast.add({ title: 'Slack OAuth cleared', icon: 'i-lucide-check', color: 'success' })
187
+ toast.add({
188
+ title: t('layout.accountDeployment.slack.cleared'),
189
+ icon: 'i-lucide-check',
190
+ color: 'success',
191
+ })
71
192
  } catch (e) {
72
193
  toast.add({
73
- title: 'Could not clear Slack OAuth',
194
+ title: t('layout.accountDeployment.slack.clearFailed'),
74
195
  description: e instanceof Error ? e.message : String(e),
75
196
  color: 'error',
76
197
  })
@@ -83,7 +204,7 @@ async function saveWeb() {
83
204
  const brave = web.braveApiKey.trim()
84
205
  const searxng = web.searxngUrl.trim()
85
206
  if (!brave && !searxng) {
86
- toast.add({ title: 'Enter a Brave key or a SearXNG URL', color: 'error' })
207
+ toast.add({ title: t('layout.accountDeployment.web.validation'), color: 'error' })
87
208
  return
88
209
  }
89
210
  savingWeb.value = true
@@ -100,10 +221,14 @@ async function saveWeb() {
100
221
  web.braveApiKey = ''
101
222
  web.searxngUrl = ''
102
223
  web.searxngApiKey = ''
103
- toast.add({ title: 'Web search keys saved', icon: 'i-lucide-check', color: 'success' })
224
+ toast.add({
225
+ title: t('layout.accountDeployment.web.saved'),
226
+ icon: 'i-lucide-check',
227
+ color: 'success',
228
+ })
104
229
  } catch (e) {
105
230
  toast.add({
106
- title: 'Could not save web search keys',
231
+ title: t('layout.accountDeployment.web.saveFailed'),
107
232
  description: e instanceof Error ? e.message : String(e),
108
233
  color: 'error',
109
234
  })
@@ -116,10 +241,14 @@ async function clearWeb() {
116
241
  savingWeb.value = true
117
242
  try {
118
243
  await store.save(props.accountId, { secrets: { webSearch: null } })
119
- toast.add({ title: 'Web search keys cleared', icon: 'i-lucide-check', color: 'success' })
244
+ toast.add({
245
+ title: t('layout.accountDeployment.web.cleared'),
246
+ icon: 'i-lucide-check',
247
+ color: 'success',
248
+ })
120
249
  } catch (e) {
121
250
  toast.add({
122
- title: 'Could not clear web search keys',
251
+ title: t('layout.accountDeployment.web.clearFailed'),
123
252
  description: e instanceof Error ? e.message : String(e),
124
253
  color: 'error',
125
254
  })
@@ -132,38 +261,50 @@ async function clearWeb() {
132
261
  <template>
133
262
  <div v-if="store.available !== false" class="space-y-6">
134
263
  <div>
135
- <h3 class="mb-1 font-semibold text-white">Deployment integrations</h3>
264
+ <h3 class="mb-1 font-semibold text-white">{{ t('layout.accountDeployment.title') }}</h3>
136
265
  <p class="text-[11px] text-slate-400">
137
- Credentials shared by every workspace in this account, sealed at rest. Values are never
138
- shown after saving; leave a field blank to keep the stored secret.
266
+ {{ t('layout.accountDeployment.intro') }}
139
267
  </p>
140
268
  </div>
141
269
 
142
270
  <!-- Slack app OAuth -->
143
271
  <section class="space-y-2">
144
272
  <div class="flex items-center gap-2">
145
- <h4 class="text-sm font-semibold text-slate-200">Slack app (OAuth)</h4>
273
+ <h4 class="text-sm font-semibold text-slate-200">
274
+ {{ t('layout.accountDeployment.slack.title') }}
275
+ </h4>
146
276
  <UBadge
147
277
  :color="summary?.slackOAuthConfigured ? 'success' : 'neutral'"
148
278
  variant="subtle"
149
279
  size="xs"
150
280
  >
151
- {{ summary?.slackOAuthConfigured ? 'Configured' : 'Not set' }}
281
+ {{
282
+ summary?.slackOAuthConfigured
283
+ ? t('layout.accountDeployment.configured')
284
+ : t('layout.accountDeployment.notSet')
285
+ }}
152
286
  </UBadge>
153
287
  </div>
154
288
  <p class="text-[11px] text-slate-400">
155
- Enables the "Add to Slack" OAuth flow. Without it, workspaces can still connect Slack by
156
- pasting a bot token.
289
+ {{ t('layout.accountDeployment.slack.description') }}
157
290
  </p>
158
291
  <div class="grid grid-cols-1 gap-2 sm:grid-cols-3">
159
- <UInput v-model="slack.clientId" placeholder="Client ID" size="sm" />
292
+ <UInput
293
+ v-model="slack.clientId"
294
+ :placeholder="t('layout.accountDeployment.slack.clientId')"
295
+ size="sm"
296
+ />
160
297
  <UInput
161
298
  v-model="slack.clientSecret"
162
299
  type="password"
163
- placeholder="Client secret"
300
+ :placeholder="t('layout.accountDeployment.slack.clientSecret')"
301
+ size="sm"
302
+ />
303
+ <UInput
304
+ v-model="slack.redirectUrl"
305
+ :placeholder="t('layout.accountDeployment.slack.redirectUrl')"
164
306
  size="sm"
165
307
  />
166
- <UInput v-model="slack.redirectUrl" placeholder="Redirect URL" size="sm" />
167
308
  </div>
168
309
  <div class="flex gap-2">
169
310
  <UButton
@@ -173,7 +314,7 @@ async function clearWeb() {
173
314
  :loading="savingSlack"
174
315
  @click="saveSlack"
175
316
  >
176
- Save
317
+ {{ t('common.save') }}
177
318
  </UButton>
178
319
  <UButton
179
320
  v-if="summary?.slackOAuthConfigured"
@@ -183,7 +324,7 @@ async function clearWeb() {
183
324
  :loading="savingSlack"
184
325
  @click="clearSlack"
185
326
  >
186
- Clear
327
+ {{ t('layout.accountDeployment.clear') }}
187
328
  </UButton>
188
329
  </div>
189
330
  </section>
@@ -191,22 +332,36 @@ async function clearWeb() {
191
332
  <!-- Web search keys -->
192
333
  <section class="space-y-2 border-t border-slate-800 pt-6">
193
334
  <div class="flex items-center gap-2">
194
- <h4 class="text-sm font-semibold text-slate-200">Container web search</h4>
335
+ <h4 class="text-sm font-semibold text-slate-200">
336
+ {{ t('layout.accountDeployment.web.title') }}
337
+ </h4>
195
338
  <UBadge :color="summary?.webSearch ? 'success' : 'neutral'" variant="subtle" size="xs">
196
- {{ summary?.webSearch ? `Configured (${summary.webSearch})` : 'Not set' }}
339
+ {{
340
+ summary?.webSearch
341
+ ? t('layout.accountDeployment.web.configured', { provider: summary.webSearch })
342
+ : t('layout.accountDeployment.notSet')
343
+ }}
197
344
  </UBadge>
198
345
  </div>
199
346
  <p class="text-[11px] text-slate-400">
200
- The search upstream container agents reach through the backend proxy. Set a Brave key
201
- (recommended), or a self-hosted SearXNG URL (with an optional bearer key).
347
+ {{ t('layout.accountDeployment.web.description') }}
202
348
  </p>
203
349
  <div class="grid grid-cols-1 gap-2 sm:grid-cols-3">
204
- <UInput v-model="web.braveApiKey" type="password" placeholder="Brave API key" size="sm" />
205
- <UInput v-model="web.searxngUrl" placeholder="SearXNG URL" size="sm" />
350
+ <UInput
351
+ v-model="web.braveApiKey"
352
+ type="password"
353
+ :placeholder="t('layout.accountDeployment.web.braveKey')"
354
+ size="sm"
355
+ />
356
+ <UInput
357
+ v-model="web.searxngUrl"
358
+ :placeholder="t('layout.accountDeployment.web.searxngUrl')"
359
+ size="sm"
360
+ />
206
361
  <UInput
207
362
  v-model="web.searxngApiKey"
208
363
  type="password"
209
- placeholder="SearXNG key (optional)"
364
+ :placeholder="t('layout.accountDeployment.web.searxngKey')"
210
365
  size="sm"
211
366
  />
212
367
  </div>
@@ -218,7 +373,7 @@ async function clearWeb() {
218
373
  :loading="savingWeb"
219
374
  @click="saveWeb"
220
375
  >
221
- Save
376
+ {{ t('common.save') }}
222
377
  </UButton>
223
378
  <UButton
224
379
  v-if="summary?.webSearch"
@@ -228,7 +383,122 @@ async function clearWeb() {
228
383
  :loading="savingWeb"
229
384
  @click="clearWeb"
230
385
  >
231
- Clear
386
+ {{ t('layout.accountDeployment.clear') }}
387
+ </UButton>
388
+ </div>
389
+ </section>
390
+
391
+ <!-- Content storage (binary artifacts / screenshots) -->
392
+ <section v-if="storageCapability" class="space-y-2 border-t border-slate-800 pt-6">
393
+ <div class="flex items-center gap-2">
394
+ <h4 class="text-sm font-semibold text-slate-200">
395
+ {{ t('layout.accountDeployment.contentStorage.title') }}
396
+ </h4>
397
+ <UBadge
398
+ :color="
399
+ storageSummary?.backend && storageSummary.backend !== 'off' ? 'success' : 'neutral'
400
+ "
401
+ variant="subtle"
402
+ size="xs"
403
+ >
404
+ {{
405
+ storageSummary?.backend
406
+ ? contentBackendLabels[storageSummary.backend]
407
+ : t('layout.accountDeployment.contentStorage.default', {
408
+ backend: contentBackendLabels[storageCapability.defaultBackend],
409
+ })
410
+ }}
411
+ </UBadge>
412
+ </div>
413
+ <p class="text-[11px] text-slate-400">
414
+ {{ t('layout.accountDeployment.contentStorage.description') }}
415
+ </p>
416
+ <div class="grid grid-cols-1 gap-2 sm:grid-cols-2">
417
+ <USelect v-model="csBackend" :items="backendItems" value-key="value" size="sm" />
418
+ </div>
419
+
420
+ <!-- Filesystem -->
421
+ <div v-if="csBackend === 'fs'" class="grid grid-cols-1 gap-2">
422
+ <UInput
423
+ v-model="cs.basePath"
424
+ :placeholder="t('layout.accountDeployment.contentStorage.basePath')"
425
+ size="sm"
426
+ />
427
+ </div>
428
+
429
+ <!-- S3 / S3-compatible -->
430
+ <template v-if="csBackend === 's3'">
431
+ <div class="grid grid-cols-1 gap-2 sm:grid-cols-2">
432
+ <UInput
433
+ v-model="cs.region"
434
+ :placeholder="t('layout.accountDeployment.contentStorage.region')"
435
+ size="sm"
436
+ />
437
+ <UInput
438
+ v-model="cs.bucket"
439
+ :placeholder="t('layout.accountDeployment.contentStorage.bucket')"
440
+ size="sm"
441
+ />
442
+ <UInput
443
+ v-model="cs.prefix"
444
+ :placeholder="t('layout.accountDeployment.contentStorage.prefix')"
445
+ size="sm"
446
+ />
447
+ <UInput
448
+ v-model="cs.endpoint"
449
+ :placeholder="t('layout.accountDeployment.contentStorage.endpoint')"
450
+ size="sm"
451
+ />
452
+ </div>
453
+ <UCheckbox
454
+ v-model="cs.forcePathStyle"
455
+ :label="t('layout.accountDeployment.contentStorage.forcePathStyle')"
456
+ size="sm"
457
+ />
458
+ <div class="flex items-center gap-2">
459
+ <span class="text-[11px] text-slate-400">
460
+ {{ t('layout.accountDeployment.contentStorage.accessKeys') }}
461
+ </span>
462
+ <UBadge
463
+ :color="storageSummary?.s3CredentialsConfigured ? 'success' : 'neutral'"
464
+ variant="subtle"
465
+ size="xs"
466
+ >
467
+ {{
468
+ storageSummary?.s3CredentialsConfigured
469
+ ? t('layout.accountDeployment.configured')
470
+ : t('layout.accountDeployment.notSet')
471
+ }}
472
+ </UBadge>
473
+ </div>
474
+ <div class="grid grid-cols-1 gap-2 sm:grid-cols-2">
475
+ <UInput
476
+ v-model="cs.accessKeyId"
477
+ type="password"
478
+ :placeholder="t('layout.accountDeployment.contentStorage.accessKeyId')"
479
+ size="sm"
480
+ />
481
+ <UInput
482
+ v-model="cs.secretAccessKey"
483
+ type="password"
484
+ :placeholder="t('layout.accountDeployment.contentStorage.secretAccessKey')"
485
+ size="sm"
486
+ />
487
+ </div>
488
+ <p class="text-[11px] text-slate-400">
489
+ {{ t('layout.accountDeployment.contentStorage.keysHint') }}
490
+ </p>
491
+ </template>
492
+
493
+ <div class="flex gap-2">
494
+ <UButton
495
+ color="primary"
496
+ size="xs"
497
+ icon="i-lucide-save"
498
+ :loading="savingStorage"
499
+ @click="saveStorage"
500
+ >
501
+ {{ t('common.save') }}
232
502
  </UButton>
233
503
  </div>
234
504
  </section>
@@ -8,6 +8,7 @@ import FragmentLibraryManager from '~/components/fragments/FragmentLibraryManage
8
8
 
9
9
  const props = defineProps<{ accountId: string }>()
10
10
  const workspace = useWorkspaceStore()
11
+ const { t } = useI18n()
11
12
 
12
13
  // Account-tier document fragments are fetched through a board's stored
13
14
  // document-source connection (credentials are per-workspace). Prefer the active
@@ -22,9 +23,7 @@ const viaWorkspaceId = computed(() => {
22
23
  <div class="space-y-6 text-sm">
23
24
  <section>
24
25
  <p class="mb-3 text-[11px] text-slate-400">
25
- Best-practice guidelines shared by every board in this account. Each board inherits these
26
- and can override or add its own. Code-aware agents (coder, reviewer, architect, fixers) fold
27
- the relevant ones into their prompt.
26
+ {{ t('layout.accountFragments.intro') }}
28
27
  </p>
29
28
  <FragmentLibraryManager
30
29
  kind="account"