@cat-factory/app 0.98.0 → 0.100.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.
@@ -1,8 +1,9 @@
1
1
  <script setup lang="ts">
2
2
  // The newest-first list of failed-attempt entries (timestamp + message + hint + collapsible
3
- // detail), shared by the task-inspector's "previous errors" disclosure (AgentFailureHistory)
4
- // and the step-detail overlay's per-step "execution history". Presentational only the caller
5
- // decides which trail to pass (the whole run's, or one step's) and how to reveal it.
3
+ // detail) behind the task-inspector's "previous errors" disclosure (AgentFailureHistory).
4
+ // Presentational only — the caller decides which trail to pass and how to reveal it. (The
5
+ // step-detail overlay's per-step "execution history" uses StepExecutionHistory instead, which
6
+ // merges these failures with the successful outputs a restart superseded.)
6
7
  import type { AgentFailure } from '~/types/domain'
7
8
  import FailureDetail from '~/components/board/FailureDetail.vue'
8
9
 
@@ -0,0 +1,100 @@
1
+ <script setup lang="ts">
2
+ // The step-detail overlay's per-step "execution history": a newest-first, MERGED timeline of
3
+ // this step's SUCCESSFUL prior outputs (discarded by a restart) and its FAILED attempts — so
4
+ // the history surfaces what superseded attempts PRODUCED, not only the errors. Presentational
5
+ // only: the caller passes both trails already narrowed to the step (by `stepIndex`).
6
+ import type { AgentFailure, PriorStepOutput } from '~/types/domain'
7
+ import FailureDetail from '~/components/board/FailureDetail.vue'
8
+ import CopyButton from '~/components/common/CopyButton.vue'
9
+
10
+ const props = defineProps<{ failures: AgentFailure[]; outputs: PriorStepOutput[] }>()
11
+
12
+ const { t, d } = useI18n()
13
+
14
+ type Entry =
15
+ | { kind: 'failure'; key: string; occurredAt: number; failure: AgentFailure }
16
+ | { kind: 'success'; key: string; occurredAt: number; output: PriorStepOutput }
17
+
18
+ // Merge both trails and show newest first — the most recent attempt is the most relevant.
19
+ // Each entry's `key` is its position within its OWN trail (both are append-only, so that
20
+ // position is a stable identity), not the volatile merged-sort index — and it stays unique
21
+ // even when several entries share a timestamp (a restart can discard many steps with the same
22
+ // clock-fallback `occurredAt`).
23
+ const entries = computed<Entry[]>(() =>
24
+ [
25
+ ...props.failures.map(
26
+ (failure, i): Entry => ({
27
+ kind: 'failure',
28
+ key: `failure-${i}`,
29
+ occurredAt: failure.occurredAt,
30
+ failure,
31
+ }),
32
+ ),
33
+ ...props.outputs.map(
34
+ (output, i): Entry => ({
35
+ kind: 'success',
36
+ key: `success-${i}`,
37
+ occurredAt: output.occurredAt,
38
+ output,
39
+ }),
40
+ ),
41
+ ].sort((a, b) => b.occurredAt - a.occurredAt),
42
+ )
43
+ </script>
44
+
45
+ <template>
46
+ <ol class="space-y-2">
47
+ <li
48
+ v-for="entry in entries"
49
+ :key="entry.key"
50
+ class="rounded-md border px-2.5 py-2"
51
+ :class="
52
+ entry.kind === 'success'
53
+ ? 'border-emerald-900/60 bg-emerald-950/20'
54
+ : 'border-slate-800/80 bg-slate-950/50'
55
+ "
56
+ :data-testid="
57
+ entry.kind === 'success' ? 'step-history-success-entry' : 'step-history-failure-entry'
58
+ "
59
+ >
60
+ <!-- a superseded SUCCESSFUL attempt: its output, collapsible + copyable -->
61
+ <template v-if="entry.kind === 'success'">
62
+ <div class="flex items-center gap-1.5 text-[10px] text-slate-500">
63
+ <UIcon name="i-lucide-check-circle-2" class="h-3 w-3 shrink-0 text-emerald-400/70" />
64
+ <time>{{ d(new Date(entry.occurredAt), 'long') }}</time>
65
+ <span class="text-emerald-400/80">{{ t('panels.stepDetail.attemptSucceeded') }}</span>
66
+ </div>
67
+ <div class="relative mt-1">
68
+ <CopyButton :text="entry.output.output" class="absolute end-1 top-1 z-10" />
69
+ <pre
70
+ class="max-h-40 overflow-auto whitespace-pre-wrap rounded bg-slate-950/80 p-1.5 pe-9 text-[10px] leading-snug text-slate-300"
71
+ >{{ entry.output.output }}</pre
72
+ >
73
+ </div>
74
+ <p v-if="entry.output.truncated" class="mt-1 text-[10px] text-slate-500">
75
+ {{ t('panels.stepDetail.outputTruncated') }}
76
+ </p>
77
+ </template>
78
+
79
+ <!-- a FAILED attempt: mirrors FailureHistoryList's entry markup -->
80
+ <template v-else>
81
+ <div class="flex items-center gap-1.5 text-[10px] text-slate-500">
82
+ <UIcon name="i-lucide-alert-triangle" class="h-3 w-3 shrink-0 text-rose-400/70" />
83
+ <time>{{ d(new Date(entry.occurredAt), 'long') }}</time>
84
+ </div>
85
+ <p class="mt-1 text-[11px] leading-snug text-slate-300" :title="entry.failure.message">
86
+ {{ entry.failure.message }}
87
+ </p>
88
+ <p v-if="entry.failure.hint" class="mt-1 text-[10px] leading-snug text-slate-500">
89
+ {{ entry.failure.hint }}
90
+ </p>
91
+ <FailureDetail
92
+ :detail="entry.failure.detail"
93
+ :message="entry.failure.message"
94
+ summary-class="text-[10px] text-slate-500 hover:text-slate-300"
95
+ pre-class="bg-slate-950/80 text-[10px] text-slate-400"
96
+ />
97
+ </template>
98
+ </li>
99
+ </ol>
100
+ </template>
@@ -11,7 +11,7 @@ import FrontendBindingsResolved from '~/components/panels/inspector/FrontendBind
11
11
  import { UI_TESTER_AGENT_KIND } from '@cat-factory/contracts'
12
12
  import ProvisioningLogsDrawer from '~/components/provisioning/ProvisioningLogsDrawer.vue'
13
13
  import IterationCapPrompt from '~/components/pipeline/IterationCapPrompt.vue'
14
- import FailureHistoryList from '~/components/board/FailureHistoryList.vue'
14
+ import StepExecutionHistory from '~/components/board/StepExecutionHistory.vue'
15
15
  import { useStepTimer } from '~/composables/useStepTimer'
16
16
  import { useStepProse } from '~/composables/useStepProse'
17
17
  import { useStepApproval } from '~/composables/useStepApproval'
@@ -96,6 +96,16 @@ const stepFailures = computed(() => {
96
96
  if (instance.value?.failure) trail.push(instance.value.failure)
97
97
  return trail.filter((f) => f.stepIndex === idx)
98
98
  })
99
+ // The positive complement of the failure trail: the SUCCESSFUL outputs a restart discarded
100
+ // for THIS step (each carries the `stepIndex` that produced it), so the history surfaces what
101
+ // superseded attempts produced — not only errors. Merged with `stepFailures` in the timeline.
102
+ const stepOutputs = computed(() => {
103
+ const idx = ctx.value?.stepIndex
104
+ if (idx == null) return []
105
+ return (instance.value?.outputHistory ?? []).filter((o) => o.stepIndex === idx)
106
+ })
107
+ // Whether this step has ANY prior-attempt history (successful outputs and/or failures).
108
+ const hasStepHistory = computed(() => stepFailures.value.length > 0 || stepOutputs.value.length > 0)
99
109
  const showHistory = ref(false)
100
110
 
101
111
  // A failed run is no longer executing: a step left mid-flight (state still
@@ -439,10 +449,10 @@ async function copyOutput() {
439
449
  />
440
450
  </div>
441
451
 
442
- <!-- this step's failure trail (the run-level history narrowed to this step),
443
- behind a toggle — mirrors the "previous errors" history on the task inspector
444
- but scoped to the step the user is looking at -->
445
- <div v-if="stepFailures.length">
452
+ <!-- this step's execution history (the run-level trail narrowed to this step),
453
+ behind a toggle — a merged timeline of the SUCCESSFUL outputs a restart
454
+ superseded and the FAILED attempts, scoped to the step being looked at -->
455
+ <div v-if="hasStepHistory">
446
456
  <UButton
447
457
  :icon="showHistory ? 'i-lucide-chevron-up' : 'i-lucide-history'"
448
458
  variant="ghost"
@@ -460,10 +470,11 @@ async function copyOutput() {
460
470
  : t('panels.stepDetail.executionHistory')
461
471
  }}
462
472
  </UButton>
463
- <FailureHistoryList
473
+ <StepExecutionHistory
464
474
  v-if="showHistory"
465
475
  class="mt-2"
466
476
  :failures="stepFailures"
477
+ :outputs="stepOutputs"
467
478
  data-testid="step-execution-history"
468
479
  />
469
480
  </div>
@@ -290,7 +290,16 @@ const detecting = ref(false)
290
290
  // message (the backend now raises an actionable one for an unreadable repo) so the user sees why
291
291
  // detection failed instead of a fixed, vague line.
292
292
  const detectError = ref<string | null>(null)
293
+ // Set instead of `detectError` when detection fails because the ephemeral-environment
294
+ // integration is turned off for this deployment (the backend 503s with code `unavailable`).
295
+ // That's a deployment-level toggle, NOT a repo/GitHub problem, so it gets its own actionable
296
+ // panel (what's off + how to enable it + a docs link) rather than the generic red line.
297
+ const detectUnavailable = ref(false)
293
298
  const detectResult = ref<ProvisioningRecommendation | null>(null)
299
+ // Where enabling the ephemeral-environment integration is documented (a deployment-level
300
+ // toggle set by whoever runs the server, so there is no in-app config page to link to).
301
+ const ENVIRONMENTS_DOCS_URL =
302
+ 'https://github.com/kibertoad/cat-factory/blob/main/backend/docs/environments-integration.md'
294
303
  // Advisory, LOCAL-ONLY selection: which compose `services:` key the user picked. It is NOT persisted
295
304
  // (the compose backend targets the file, not a single service), so it lives only in component state
296
305
  // and merely drives the chip highlight. Without it the highlight would compare `composePath` — which
@@ -304,6 +313,7 @@ watch(
304
313
  () => {
305
314
  detectResult.value = null
306
315
  detectError.value = null
316
+ detectUnavailable.value = false
307
317
  pickedComposeService.value = null
308
318
  },
309
319
  )
@@ -321,6 +331,7 @@ async function detectFromRepo() {
321
331
  }
322
332
  detecting.value = true
323
333
  detectError.value = null
334
+ detectUnavailable.value = false
324
335
  try {
325
336
  const rec = await infra.detectProvisioning({
326
337
  owner: repo.owner,
@@ -351,12 +362,18 @@ async function detectFromRepo() {
351
362
  if (rec.provisioning.type === 'kubernetes') seedKubeSource(rec.provisioning.manifestSource)
352
363
  }
353
364
  } catch (e) {
354
- // Surface the server's real message (an actionable "couldn't read the repo — check App access"
355
- // for a read fault), falling back to the generic line only when none is available.
356
- detectError.value =
357
- apiErrorEnvelope(e)?.message ??
358
- (e instanceof Error ? e.message : null) ??
359
- t('inspector.testConfig.detect.error')
365
+ // A 503 `unavailable` means the ephemeral-environment integration is off for this deployment
366
+ // (not a repo read fault) show the dedicated "how to enable it" panel instead of a red line.
367
+ if (apiErrorEnvelope(e)?.code === 'unavailable') {
368
+ detectUnavailable.value = true
369
+ } else {
370
+ // Surface the server's real message (an actionable "couldn't read the repo — check App access"
371
+ // for a read fault), falling back to the generic line only when none is available.
372
+ detectError.value =
373
+ apiErrorEnvelope(e)?.message ??
374
+ (e instanceof Error ? e.message : null) ??
375
+ t('inspector.testConfig.detect.error')
376
+ }
360
377
  } finally {
361
378
  detecting.value = false
362
379
  }
@@ -496,6 +513,28 @@ function setSize(value: InstanceSize) {
496
513
  {{ detectError }}
497
514
  </p>
498
515
 
516
+ <!-- The ephemeral-environment integration is off for this deployment. Say exactly what's
517
+ missing (it's separate from the GitHub connection), what enables it, and link the docs. -->
518
+ <div
519
+ v-if="detectUnavailable"
520
+ class="space-y-1 rounded border border-amber-500/30 bg-amber-500/5 p-2"
521
+ >
522
+ <p class="text-[11px] font-medium text-amber-300/90">
523
+ {{ t('inspector.testConfig.detect.unavailable.title') }}
524
+ </p>
525
+ <p class="text-[11px] leading-snug text-slate-400">
526
+ {{ t('inspector.testConfig.detect.unavailable.body') }}
527
+ </p>
528
+ <a
529
+ :href="ENVIRONMENTS_DOCS_URL"
530
+ target="_blank"
531
+ rel="noopener noreferrer"
532
+ class="inline-block text-[11px] text-primary-400 underline hover:text-primary-300"
533
+ >
534
+ {{ t('inspector.testConfig.detect.unavailable.docs') }}
535
+ </a>
536
+ </div>
537
+
499
538
  <template v-if="detectResult && !detecting">
500
539
  <p
501
540
  v-if="!detectResult.detected && detectResult.provisioning.type !== 'custom'"
@@ -0,0 +1,294 @@
1
+ <script setup lang="ts">
2
+ // The Budget configuration screen: the three spend-budget tiers (workspace, account,
3
+ // user). The workspace tier is a per-workspace monthly limit + currency; the account and
4
+ // user tiers are monthly ceilings that gate a run when EITHER is exhausted. When the
5
+ // operator sets a hard cap env var, the account/user input cannot exceed it and the cap is
6
+ // shown here. See docs/initiatives/tiered-budgets.md.
7
+ import { computed, reactive, ref, watch, type Ref } from 'vue'
8
+
9
+ const { t, n } = useI18n()
10
+ const toast = useToast()
11
+
12
+ const settingsStore = useWorkspaceSettingsStore()
13
+ const userSettingsStore = useUserSettingsStore()
14
+ const accounts = useAccountsStore()
15
+ const workspace = useWorkspaceStore()
16
+
17
+ const caps = computed(() => workspace.budgetCaps)
18
+ const capCurrency = computed(() => caps.value?.currency ?? 'EUR')
19
+ // Format an amount in a given currency. The account/user tiers are in the base pricing
20
+ // currency (`capCurrency`); the workspace tier is in its OWN overridden `spend.currency`, so
21
+ // its callers pass that in — otherwise a USD workspace on a EUR deployment renders `€` on USD.
22
+ const money = (value: number, currency: string = capCurrency.value) =>
23
+ n(value, { key: 'currency', currency })
24
+
25
+ // Persist one tier's budget: save, toast success, then best-effort refresh the snapshot AFTER
26
+ // the save has succeeded. A transient snapshot-refresh failure must NOT report a persisted
27
+ // budget as failed (the spend meter also catches up on the next pushed snapshot); a genuine
28
+ // save rejection surfaces its message so the user sees why (e.g. the operator hard-cap reject).
29
+ async function runSave(saving: Ref<boolean>, save: () => Promise<unknown>) {
30
+ saving.value = true
31
+ try {
32
+ await save()
33
+ toast.add({ title: t('settings.workspaceSettings.toast.budgetSaved'), color: 'success' })
34
+ try {
35
+ await useWorkspaceStore().refresh()
36
+ } catch {
37
+ // ignore — the budget is persisted; the meter will catch up on the next snapshot.
38
+ }
39
+ } catch (e) {
40
+ toast.add({
41
+ title: t('settings.workspaceSettings.toast.budgetSaveFailed'),
42
+ description: e instanceof Error ? e.message : String(e),
43
+ color: 'error',
44
+ })
45
+ } finally {
46
+ saving.value = false
47
+ }
48
+ }
49
+
50
+ // ---- Workspace tier -------------------------------------------------------
51
+ const wsDraft = reactive({ spendCurrency: '', spendMonthlyLimit: '' })
52
+ function hydrateWorkspace() {
53
+ const s = settingsStore.settings
54
+ wsDraft.spendCurrency = s.spendCurrency ?? ''
55
+ wsDraft.spendMonthlyLimit = s.spendMonthlyLimit == null ? '' : String(s.spendMonthlyLimit)
56
+ }
57
+ watch(() => settingsStore.settings, hydrateWorkspace, { immediate: true })
58
+
59
+ const savingWorkspace = ref(false)
60
+ function saveWorkspace() {
61
+ const raw = String(wsDraft.spendMonthlyLimit ?? '').trim()
62
+ return runSave(savingWorkspace, () =>
63
+ settingsStore.update({
64
+ spendCurrency: wsDraft.spendCurrency.trim()
65
+ ? wsDraft.spendCurrency.trim().toUpperCase()
66
+ : null,
67
+ spendMonthlyLimit: raw === '' ? null : Number(raw),
68
+ }),
69
+ )
70
+ }
71
+
72
+ // ---- Account tier ---------------------------------------------------------
73
+ const account = computed(() => accounts.activeAccount)
74
+ const canEditAccount = computed(() => account.value?.roles?.includes('admin') ?? false)
75
+ const accountCap = computed(() => caps.value?.accountMonthlyLimitMax ?? null)
76
+ const accountDraft = ref('')
77
+ watch(
78
+ account,
79
+ (a) => {
80
+ accountDraft.value = a?.spendMonthlyLimit == null ? '' : String(a.spendMonthlyLimit)
81
+ },
82
+ { immediate: true },
83
+ )
84
+ const accountOverCap = computed(
85
+ () =>
86
+ accountCap.value != null &&
87
+ accountDraft.value.trim() !== '' &&
88
+ Number(accountDraft.value) > accountCap.value,
89
+ )
90
+ const savingAccount = ref(false)
91
+ function saveAccount() {
92
+ const acc = account.value
93
+ if (!acc || accountOverCap.value) return
94
+ const raw = accountDraft.value.trim()
95
+ return runSave(savingAccount, () =>
96
+ accounts.setSpendMonthlyLimit(acc.id, raw === '' ? null : Number(raw)),
97
+ )
98
+ }
99
+
100
+ // ---- User tier ------------------------------------------------------------
101
+ const userCap = computed(() => caps.value?.userMonthlyLimitMax ?? null)
102
+ const userDraft = ref('')
103
+ watch(
104
+ () => userSettingsStore.settings,
105
+ (s) => {
106
+ userDraft.value = s.spendMonthlyLimit == null ? '' : String(s.spendMonthlyLimit)
107
+ },
108
+ { immediate: true },
109
+ )
110
+ const userOverCap = computed(
111
+ () =>
112
+ userCap.value != null &&
113
+ userDraft.value.trim() !== '' &&
114
+ Number(userDraft.value) > userCap.value,
115
+ )
116
+ const savingUser = ref(false)
117
+ function saveUser() {
118
+ if (userOverCap.value) return
119
+ const raw = userDraft.value.trim()
120
+ return runSave(savingUser, () =>
121
+ userSettingsStore.update({ spendMonthlyLimit: raw === '' ? null : Number(raw) }),
122
+ )
123
+ }
124
+ </script>
125
+
126
+ <template>
127
+ <div class="space-y-8">
128
+ <p class="text-[11px] text-slate-400">
129
+ {{ t('settings.workspaceSettings.budget.body') }}
130
+ </p>
131
+
132
+ <!-- Workspace tier -->
133
+ <section class="space-y-2">
134
+ <h3 class="text-sm font-semibold text-slate-200">
135
+ {{ t('settings.workspaceSettings.budget.workspace') }}
136
+ </h3>
137
+ <div class="grid grid-cols-2 gap-3">
138
+ <label class="block">
139
+ <span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
140
+ {{ t('settings.workspaceSettings.budget.monthlyLimit') }}
141
+ </span>
142
+ <UInput
143
+ v-model="wsDraft.spendMonthlyLimit"
144
+ type="number"
145
+ :min="0"
146
+ :placeholder="t('settings.workspaceSettings.budget.defaultPlaceholder')"
147
+ size="sm"
148
+ />
149
+ </label>
150
+ <label class="block">
151
+ <span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
152
+ {{ t('settings.workspaceSettings.budget.currency') }}
153
+ </span>
154
+ <UInput
155
+ v-model="wsDraft.spendCurrency"
156
+ placeholder="EUR"
157
+ maxlength="3"
158
+ size="sm"
159
+ class="uppercase"
160
+ />
161
+ </label>
162
+ </div>
163
+ <div v-if="workspace.spend" class="text-[11px] text-slate-400">
164
+ {{
165
+ t('settings.workspaceSettings.budget.spent', {
166
+ spent: money(workspace.spend.costSpent, workspace.spend.currency),
167
+ limit: money(workspace.spend.costLimit, workspace.spend.currency),
168
+ })
169
+ }}
170
+ </div>
171
+ <div class="flex justify-end">
172
+ <UButton
173
+ color="primary"
174
+ icon="i-lucide-save"
175
+ size="sm"
176
+ :loading="savingWorkspace"
177
+ @click="saveWorkspace"
178
+ >
179
+ {{ t('settings.workspaceSettings.budget.saveTier') }}
180
+ </UButton>
181
+ </div>
182
+ </section>
183
+
184
+ <!-- Account tier -->
185
+ <section v-if="account" class="space-y-2">
186
+ <h3 class="text-sm font-semibold text-slate-200">
187
+ {{ t('settings.workspaceSettings.budget.account') }}
188
+ </h3>
189
+ <p class="text-[11px] text-slate-400">
190
+ {{ t('settings.workspaceSettings.budget.accountBody') }}
191
+ </p>
192
+ <label class="block">
193
+ <span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
194
+ {{ t('settings.workspaceSettings.budget.monthlyLimit') }}
195
+ </span>
196
+ <UInput
197
+ v-model="accountDraft"
198
+ type="number"
199
+ :min="0"
200
+ :max="accountCap ?? undefined"
201
+ :disabled="!canEditAccount"
202
+ :placeholder="t('settings.workspaceSettings.budget.noLimitPlaceholder')"
203
+ size="sm"
204
+ />
205
+ </label>
206
+ <p
207
+ v-if="accountCap != null"
208
+ class="text-[11px]"
209
+ :class="accountOverCap ? 'text-rose-400' : 'text-amber-400'"
210
+ >
211
+ {{ t('settings.workspaceSettings.budget.hardCap', { amount: money(accountCap) }) }}
212
+ <span class="text-slate-500"
213
+ >({{ t('settings.workspaceSettings.budget.hardCapHint') }})</span
214
+ >
215
+ </p>
216
+ <div v-if="workspace.accountSpend" class="text-[11px] text-slate-400">
217
+ {{
218
+ t('settings.workspaceSettings.budget.spent', {
219
+ spent: money(workspace.accountSpend.costSpent),
220
+ limit: money(workspace.accountSpend.costLimit),
221
+ })
222
+ }}
223
+ </div>
224
+ <p v-if="!canEditAccount" class="text-[11px] text-slate-500">
225
+ {{ t('settings.workspaceSettings.budget.adminOnly') }}
226
+ </p>
227
+ <div v-if="canEditAccount" class="flex justify-end">
228
+ <UButton
229
+ color="primary"
230
+ icon="i-lucide-save"
231
+ size="sm"
232
+ :loading="savingAccount"
233
+ :disabled="accountOverCap"
234
+ @click="saveAccount"
235
+ >
236
+ {{ t('settings.workspaceSettings.budget.saveTier') }}
237
+ </UButton>
238
+ </div>
239
+ </section>
240
+
241
+ <!-- User tier -->
242
+ <section class="space-y-2">
243
+ <h3 class="text-sm font-semibold text-slate-200">
244
+ {{ t('settings.workspaceSettings.budget.user') }}
245
+ </h3>
246
+ <p class="text-[11px] text-slate-400">
247
+ {{ t('settings.workspaceSettings.budget.userBody') }}
248
+ </p>
249
+ <label class="block">
250
+ <span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
251
+ {{ t('settings.workspaceSettings.budget.monthlyLimit') }}
252
+ </span>
253
+ <UInput
254
+ v-model="userDraft"
255
+ type="number"
256
+ :min="0"
257
+ :max="userCap ?? undefined"
258
+ :placeholder="t('settings.workspaceSettings.budget.noLimitPlaceholder')"
259
+ size="sm"
260
+ />
261
+ </label>
262
+ <p
263
+ v-if="userCap != null"
264
+ class="text-[11px]"
265
+ :class="userOverCap ? 'text-rose-400' : 'text-amber-400'"
266
+ >
267
+ {{ t('settings.workspaceSettings.budget.hardCap', { amount: money(userCap) }) }}
268
+ <span class="text-slate-500"
269
+ >({{ t('settings.workspaceSettings.budget.hardCapHint') }})</span
270
+ >
271
+ </p>
272
+ <div v-if="workspace.userSpend" class="text-[11px] text-slate-400">
273
+ {{
274
+ t('settings.workspaceSettings.budget.spent', {
275
+ spent: money(workspace.userSpend.costSpent),
276
+ limit: money(workspace.userSpend.costLimit),
277
+ })
278
+ }}
279
+ </div>
280
+ <div class="flex justify-end">
281
+ <UButton
282
+ color="primary"
283
+ icon="i-lucide-save"
284
+ size="sm"
285
+ :loading="savingUser"
286
+ :disabled="userOverCap"
287
+ @click="saveUser"
288
+ >
289
+ {{ t('settings.workspaceSettings.budget.saveTier') }}
290
+ </UButton>
291
+ </div>
292
+ </section>
293
+ </div>
294
+ </template>
@@ -12,6 +12,7 @@ import type { CreateTaskType, TaskLimitMode } from '~/types/domain'
12
12
  import MergeThresholdsPanel from '~/components/settings/MergeThresholdsPanel.vue'
13
13
  import IssueTrackerPanel from '~/components/settings/IssueTrackerPanel.vue'
14
14
  import ServiceFragmentDefaultsPanel from '~/components/settings/ServiceFragmentDefaultsPanel.vue'
15
+ import BudgetSettings from '~/components/settings/BudgetSettings.vue'
15
16
  import IntegrationBackTitle from '~/components/layout/IntegrationBackTitle.vue'
16
17
 
17
18
  const { t, te } = useI18n()
@@ -114,9 +115,6 @@ const draft = reactive({
114
115
  storeAgentContext: true,
115
116
  artifactRetentionDays: 14,
116
117
  kaizenEnabled: true,
117
- // Budget: empty string ⇒ "use the built-in default" (null on the wire).
118
- spendCurrency: '',
119
- spendMonthlyLimit: '',
120
118
  })
121
119
 
122
120
  function hydrate() {
@@ -129,8 +127,6 @@ function hydrate() {
129
127
  draft.storeAgentContext = s.storeAgentContext
130
128
  draft.artifactRetentionDays = s.artifactRetentionDays
131
129
  draft.kaizenEnabled = s.kaizenEnabled
132
- draft.spendCurrency = s.spendCurrency ?? ''
133
- draft.spendMonthlyLimit = s.spendMonthlyLimit == null ? '' : String(s.spendMonthlyLimit)
134
130
  }
135
131
 
136
132
  // `store.settings` is always replaced wholesale (store hydrate/update reassign the ref),
@@ -176,46 +172,6 @@ async function save() {
176
172
  saving.value = false
177
173
  }
178
174
  }
179
-
180
- const savingBudget = ref(false)
181
-
182
- async function saveBudget() {
183
- savingBudget.value = true
184
- // The number input emits a raw number once edited but starts as a string from hydrate, so
185
- // coerce through String() before trimming. Blank ⇒ "use the built-in default" (null on the wire).
186
- const raw = String(draft.spendMonthlyLimit ?? '').trim()
187
- const monthlyLimit = raw === '' ? null : Number(raw)
188
- try {
189
- await store.update({
190
- spendCurrency: draft.spendCurrency.trim() ? draft.spendCurrency.trim().toUpperCase() : null,
191
- spendMonthlyLimit: monthlyLimit,
192
- })
193
- toast.add({
194
- title: t('settings.workspaceSettings.toast.budgetSaved'),
195
- icon: 'i-lucide-check',
196
- color: 'success',
197
- })
198
- // The settings PUT only returns the settings; re-fetch the snapshot so the toolbar's
199
- // spend meter reflects the newly-set limit/currency (spendService.status) right away.
200
- // Best-effort and AFTER the save succeeded: a transient snapshot-refresh failure must
201
- // not report a successfully-saved budget as failed (the meter also catches up on the
202
- // next snapshot pushed over the stream).
203
- try {
204
- await useWorkspaceStore().refresh()
205
- } catch {
206
- // ignore — the budget is persisted; the meter will catch up on the next snapshot.
207
- }
208
- } catch (e) {
209
- toast.add({
210
- title: t('settings.workspaceSettings.toast.budgetSaveFailed'),
211
- description: e instanceof Error ? e.message : String(e),
212
- icon: 'i-lucide-triangle-alert',
213
- color: 'error',
214
- })
215
- } finally {
216
- savingBudget.value = false
217
- }
218
- }
219
175
  </script>
220
176
 
221
177
  <template>
@@ -370,56 +326,9 @@ async function saveBudget() {
370
326
  </div>
371
327
  </template>
372
328
 
373
- <!-- Budget -->
329
+ <!-- Budget (workspace / account / user tiers) -->
374
330
  <template #budget>
375
- <div class="space-y-6">
376
- <section class="space-y-2">
377
- <h3 class="text-sm font-semibold text-slate-200">
378
- {{ t('settings.workspaceSettings.budget.heading') }}
379
- </h3>
380
- <p class="text-[11px] text-slate-400">
381
- {{ t('settings.workspaceSettings.budget.body') }}
382
- </p>
383
- <div class="grid grid-cols-2 gap-3">
384
- <label class="block">
385
- <span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
386
- {{ t('settings.workspaceSettings.budget.monthlyLimit') }}
387
- </span>
388
- <UInput
389
- v-model="draft.spendMonthlyLimit"
390
- type="number"
391
- :min="0"
392
- :placeholder="t('settings.workspaceSettings.budget.defaultPlaceholder')"
393
- size="sm"
394
- />
395
- </label>
396
- <label class="block">
397
- <span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
398
- {{ t('settings.workspaceSettings.budget.currency') }}
399
- </span>
400
- <UInput
401
- v-model="draft.spendCurrency"
402
- placeholder="EUR"
403
- maxlength="3"
404
- size="sm"
405
- class="uppercase"
406
- />
407
- </label>
408
- </div>
409
- </section>
410
-
411
- <div class="flex justify-end">
412
- <UButton
413
- color="primary"
414
- icon="i-lucide-save"
415
- size="sm"
416
- :loading="savingBudget"
417
- @click="saveBudget"
418
- >
419
- {{ t('settings.workspaceSettings.budget.save') }}
420
- </UButton>
421
- </div>
422
- </div>
331
+ <BudgetSettings />
423
332
  </template>
424
333
 
425
334
  <!-- Merge thresholds -->
@@ -0,0 +1,12 @@
1
+ import { getUserSettingsContract, updateUserSettingsContract } from '@cat-factory/contracts'
2
+ import type { UpdateUserSettingsInput } from '~/types/domain'
3
+ import type { ApiContext } from './context'
4
+
5
+ /** Per-user settings (the user-tier spend budget), scoped to the signed-in user. */
6
+ export function userSettingsApi({ send }: ApiContext) {
7
+ return {
8
+ getUserSettings: () => send(getUserSettingsContract, {}),
9
+ updateUserSettings: (body: UpdateUserSettingsInput) =>
10
+ send(updateUserSettingsContract, { body }),
11
+ }
12
+ }
@@ -36,6 +36,7 @@ import { slackApi } from './api/slack'
36
36
  import { specApi } from './api/spec'
37
37
  import { tasksApi } from './api/tasks'
38
38
  import { userSecretsApi } from './api/userSecrets'
39
+ import { userSettingsApi } from './api/userSettings'
39
40
  import { workspacesApi } from './api/workspaces'
40
41
 
41
42
  /**
@@ -131,5 +132,6 @@ export function useApi() {
131
132
  ...slackApi(ctx),
132
133
  ...bootstrapApi(ctx),
133
134
  ...userSecretsApi(ctx),
135
+ ...userSettingsApi(ctx),
134
136
  }
135
137
  }
@@ -67,6 +67,17 @@ export const useAccountsStore = defineStore(
67
67
  return updated
68
68
  }
69
69
 
70
+ /**
71
+ * Set an account's monthly spend budget (the account tier). Admin-only on the
72
+ * backend; `null` clears the limit. Patches the loaded account in place on success.
73
+ */
74
+ async function setSpendMonthlyLimit(id: string, limit: number | null) {
75
+ const updated = await api.updateAccount(id, { spendMonthlyLimit: limit })
76
+ const i = accounts.value.findIndex((a) => a.id === id)
77
+ if (i >= 0) accounts.value[i] = updated
78
+ return updated
79
+ }
80
+
70
81
  // ---- members + invitations -------------------------------------------
71
82
 
72
83
  const members = ref<AccountMember[]>([])
@@ -139,6 +150,7 @@ export const useAccountsStore = defineStore(
139
150
  createOrg,
140
151
  switchTo,
141
152
  setDefaultCloudProvider,
153
+ setSpendMonthlyLimit,
142
154
  loadRoster,
143
155
  invite,
144
156
  revokeInvite,
@@ -0,0 +1,28 @@
1
+ import { defineStore } from 'pinia'
2
+ import { ref } from 'vue'
3
+ import type { UpdateUserSettingsInput, UserSettings } from '~/types/domain'
4
+
5
+ const DEFAULTS: UserSettings = {
6
+ spendMonthlyLimit: null,
7
+ }
8
+
9
+ /**
10
+ * The signed-in user's personal settings (today: the user-tier spend budget). Hydrated
11
+ * from the workspace snapshot's `userSettings`; `update` persists via `PUT /user-settings`
12
+ * and patches the local copy. Empty (defaults) when no user is signed in.
13
+ */
14
+ export const useUserSettingsStore = defineStore('userSettings', () => {
15
+ const api = useApi()
16
+ const settings = ref<UserSettings>({ ...DEFAULTS })
17
+
18
+ function hydrate(value: UserSettings | null) {
19
+ settings.value = value ? { ...DEFAULTS, ...value } : { ...DEFAULTS }
20
+ }
21
+
22
+ async function update(patch: UpdateUserSettingsInput) {
23
+ settings.value = await api.updateUserSettings(patch)
24
+ return settings.value
25
+ }
26
+
27
+ return { settings, hydrate, update }
28
+ })
@@ -1,6 +1,12 @@
1
1
  import { defineStore } from 'pinia'
2
2
  import { computed, ref } from 'vue'
3
- import type { InfraSetup, SpendStatus, Workspace, WorkspaceSnapshot } from '~/types/domain'
3
+ import type {
4
+ BudgetCaps,
5
+ InfraSetup,
6
+ SpendStatus,
7
+ Workspace,
8
+ WorkspaceSnapshot,
9
+ } from '~/types/domain'
4
10
  import { useAccountsStore } from '~/stores/accounts'
5
11
  import { useBoardStore } from '~/stores/board'
6
12
  import { usePipelinesStore } from '~/stores/pipelines'
@@ -50,8 +56,14 @@ export const useWorkspaceStore = defineStore(
50
56
  const ready = ref(false)
51
57
  /** Set when bootstrap fails so the UI can show a retry. */
52
58
  const error = ref<string | null>(null)
53
- /** Latest spend-safeguard status from the server (null until first load). */
59
+ /** Latest WORKSPACE-tier spend-safeguard status from the server (null until first load). */
54
60
  const spend = ref<SpendStatus | null>(null)
61
+ /** ACCOUNT-tier spend status (null when the tier is inactive or unavailable). */
62
+ const accountSpend = ref<SpendStatus | null>(null)
63
+ /** USER-tier spend status for the signed-in caller (null when inactive). */
64
+ const userSpend = ref<SpendStatus | null>(null)
65
+ /** Operator hard ceilings on the account/user budget tiers (null until first load). */
66
+ const budgetCaps = ref<BudgetCaps | null>(null)
55
67
  /**
56
68
  * Per-area infrastructure-setup status (ephemeral environments / agent executor / binary
57
69
  * storage) from the snapshot, driving the infra-setup banner. Null on an older backend that
@@ -93,6 +105,10 @@ export const useWorkspaceStore = defineStore(
93
105
  }
94
106
  workspaceId.value = snapshot.workspace.id
95
107
  spend.value = snapshot.spend ?? null
108
+ accountSpend.value = snapshot.accountSpend ?? null
109
+ userSpend.value = snapshot.userSpend ?? null
110
+ budgetCaps.value = snapshot.budgetCaps ?? null
111
+ useUserSettingsStore().hydrate(snapshot.userSettings ?? null)
96
112
  infraSetup.value = snapshot.infraSetup ?? null
97
113
  // Keep the board list in step (e.g. a freshly created board, or a rename).
98
114
  const i = workspaces.value.findIndex((w) => w.id === snapshot.workspace.id)
@@ -254,6 +270,9 @@ export const useWorkspaceStore = defineStore(
254
270
  ready,
255
271
  error,
256
272
  spend,
273
+ accountSpend,
274
+ userSpend,
275
+ budgetCaps,
257
276
  infraSetup,
258
277
  init,
259
278
  switchTo,
@@ -57,11 +57,14 @@ export type {
57
57
  CustomAgentKind,
58
58
  Pipeline,
59
59
  SpendStatus,
60
+ BudgetCaps,
60
61
  Workspace,
61
62
  WorkspaceSnapshot,
62
63
  TaskLimitMode,
63
64
  WorkspaceSettings,
64
65
  UpdateWorkspaceSettingsInput,
66
+ UserSettings,
67
+ UpdateUserSettingsInput,
65
68
  InfraSetup,
66
69
  InfraSetupStatus,
67
70
  InfraSetupArea,
@@ -18,6 +18,7 @@ export type {
18
18
  AgentRunKind,
19
19
  AgentFailureKind,
20
20
  AgentFailure,
21
+ PriorStepOutput,
21
22
  StepApproval,
22
23
  StepMetrics,
23
24
  LlmCallMetric,
@@ -744,7 +744,12 @@
744
744
  "urlSource": "Suggested environment URL source: {source}. The workspace handler owns this; set it there.",
745
745
  "namespace": "Manifests pin namespace \"{namespace}\"; recommend honoring it on the workspace handler.",
746
746
  "confidenceHigh": "Detected",
747
- "confidenceLow": "Suggestion"
747
+ "confidenceLow": "Suggestion",
748
+ "unavailable": {
749
+ "title": "Ephemeral environments aren't enabled",
750
+ "body": "Autodetect reads this repo to suggest a test-environment (Kubernetes or Docker Compose) config, but the ephemeral-environment integration is turned off for this deployment. This is separate from your GitHub connection. Whoever runs the server enables it (set ENVIRONMENTS_ENABLED and an encryption key); then autodetect and provisioning become available.",
751
+ "docs": "How to enable ephemeral environments"
752
+ }
748
753
  },
749
754
  "envWizard": {
750
755
  "title": "Compose environment setup",
@@ -1027,6 +1032,8 @@
1027
1032
  "hideInfraAttempts": "Hide infrastructure attempts",
1028
1033
  "executionHistory": "Execution history",
1029
1034
  "hideExecutionHistory": "Hide execution history",
1035
+ "attemptSucceeded": "Succeeded",
1036
+ "outputTruncated": "Output clipped to keep the run history compact.",
1030
1037
  "editingConclusions": "Editing the conclusions",
1031
1038
  "editConclusionsPlaceholder": "Edit the agent's conclusions; your edits are saved when you approve…",
1032
1039
  "noProseOutput": "This agent produced no prose output.",
@@ -2367,11 +2374,22 @@
2367
2374
  },
2368
2375
  "budget": {
2369
2376
  "heading": "Monthly spend budget",
2370
- "body": "Token usage is metered per LLM call, priced, and gated by this budget. When reached, runs in this workspace pause and the board shows a warning. Leave blank to inherit the built-in default (about 100 EUR/month).",
2377
+ "body": "Token usage is metered per LLM call, priced, and gated by these budgets. A run pauses when any tier it belongs to is exhausted. Leave a field blank for no limit on that tier (the workspace tier then inherits the built-in default, about 100 EUR/month).",
2371
2378
  "monthlyLimit": "Monthly limit",
2372
2379
  "defaultPlaceholder": "Default",
2373
2380
  "currency": "Currency (ISO 4217)",
2374
- "save": "Save budget"
2381
+ "save": "Save budget",
2382
+ "saveTier": "Save",
2383
+ "workspace": "This workspace",
2384
+ "account": "Account (all workspaces)",
2385
+ "user": "You (all your runs)",
2386
+ "accountBody": "A ceiling across every workspace in this account.",
2387
+ "userBody": "A ceiling across every run you start, in any workspace.",
2388
+ "hardCap": "Operator limit: {amount}",
2389
+ "hardCapHint": "set by the deployment; the value can't exceed it",
2390
+ "spent": "{spent} of {limit} spent this month",
2391
+ "adminOnly": "Only an account admin can change the account budget.",
2392
+ "noLimitPlaceholder": "No limit"
2375
2393
  },
2376
2394
  "toast": {
2377
2395
  "saved": "Settings saved",
@@ -688,7 +688,12 @@
688
688
  "urlSource": "Fuente de URL del entorno sugerida: {source}. El gestor del espacio de trabajo la controla; configúrala allí.",
689
689
  "namespace": "Los manifiestos fijan el espacio de nombres \"{namespace}\"; se recomienda respetarlo en el gestor del espacio de trabajo.",
690
690
  "confidenceHigh": "Detectado",
691
- "confidenceLow": "Sugerencia"
691
+ "confidenceLow": "Sugerencia",
692
+ "unavailable": {
693
+ "title": "Los entornos efímeros no están habilitados",
694
+ "body": "La detección automática lee este repositorio para sugerir una configuración de entorno de prueba (Kubernetes o Docker Compose), pero la integración de entornos efímeros está desactivada en este despliegue. Esto es independiente de tu conexión con GitHub. Quien administre el servidor debe habilitarla (definir ENVIRONMENTS_ENABLED y una clave de cifrado); después, la detección automática y el aprovisionamiento estarán disponibles.",
695
+ "docs": "Cómo habilitar los entornos efímeros"
696
+ }
692
697
  },
693
698
  "customManifestPathHint": "Se rellena con el valor predeterminado del tipo al seleccionarlo. Usa Detectar para localizar un manifiesto existente en el repositorio.",
694
699
  "generateManifest": {
@@ -984,6 +989,8 @@
984
989
  "hideInfraAttempts": "Ocultar intentos de infraestructura",
985
990
  "executionHistory": "Historial de ejecución",
986
991
  "hideExecutionHistory": "Ocultar historial de ejecución",
992
+ "attemptSucceeded": "Correcto",
993
+ "outputTruncated": "Salida recortada para mantener compacto el historial de ejecución.",
987
994
  "editingConclusions": "Editando las conclusiones",
988
995
  "editConclusionsPlaceholder": "Edita las conclusiones del agente; tus cambios se guardan cuando apruebas…",
989
996
  "noProseOutput": "Este agente no produjo salida en prosa.",
@@ -2193,11 +2200,22 @@
2193
2200
  },
2194
2201
  "budget": {
2195
2202
  "heading": "Presupuesto de gasto mensual",
2196
- "body": "El uso de tokens se contabiliza por llamada al LLM, se valora y se limita con este presupuesto. Al alcanzarlo, las ejecuciones de este espacio de trabajo se pausan y el tablero muestra una advertencia. Déjalo en blanco para heredar el valor predeterminado integrado (unos 100 EUR/mes).",
2203
+ "body": "El uso de tokens se contabiliza por llamada al LLM, se valora y se limita con estos presupuestos. Una ejecución se pausa cuando se agota cualquier nivel al que pertenece. Deja un campo en blanco para no poner límite a ese nivel (el nivel del espacio de trabajo hereda entonces el valor predeterminado integrado, unos 100 EUR/mes).",
2197
2204
  "monthlyLimit": "Límite mensual",
2198
2205
  "defaultPlaceholder": "Predeterminado",
2199
2206
  "currency": "Moneda (ISO 4217)",
2200
- "save": "Guardar presupuesto"
2207
+ "save": "Guardar presupuesto",
2208
+ "saveTier": "Guardar",
2209
+ "workspace": "Este espacio de trabajo",
2210
+ "account": "Cuenta (todos los espacios de trabajo)",
2211
+ "user": "Tú (todas tus ejecuciones)",
2212
+ "accountBody": "Un tope para todos los espacios de trabajo de esta cuenta.",
2213
+ "userBody": "Un tope para todas las ejecuciones que inicies, en cualquier espacio de trabajo.",
2214
+ "hardCap": "Límite del operador: {amount}",
2215
+ "hardCapHint": "definido por el despliegue; el valor no puede superarlo",
2216
+ "spent": "{spent} de {limit} gastado este mes",
2217
+ "adminOnly": "Solo un administrador de la cuenta puede cambiar el presupuesto de la cuenta.",
2218
+ "noLimitPlaceholder": "Sin límite"
2201
2219
  },
2202
2220
  "toast": {
2203
2221
  "saved": "Configuración guardada",
@@ -688,7 +688,12 @@
688
688
  "urlSource": "Source d'URL d'environnement suggérée : {source}. Le gestionnaire de l'espace de travail la contrôle ; définissez-la là.",
689
689
  "namespace": "Les manifestes fixent l'espace de noms « {namespace} » ; il est recommandé de le respecter sur le gestionnaire de l'espace de travail.",
690
690
  "confidenceHigh": "Détecté",
691
- "confidenceLow": "Suggestion"
691
+ "confidenceLow": "Suggestion",
692
+ "unavailable": {
693
+ "title": "Les environnements éphémères ne sont pas activés",
694
+ "body": "La détection automatique lit ce dépôt pour proposer une configuration d'environnement de test (Kubernetes ou Docker Compose), mais l'intégration des environnements éphémères est désactivée pour ce déploiement. C'est distinct de votre connexion GitHub. La personne qui gère le serveur doit l'activer (définir ENVIRONMENTS_ENABLED et une clé de chiffrement) ; la détection automatique et le provisionnement deviennent alors disponibles.",
695
+ "docs": "Comment activer les environnements éphémères"
696
+ }
692
697
  },
693
698
  "customManifestPathHint": "Prérempli avec la valeur par défaut du type lors de sa sélection. Utilisez Détecter pour localiser un manifeste existant dans le dépôt.",
694
699
  "generateManifest": {
@@ -984,6 +989,8 @@
984
989
  "hideInfraAttempts": "Masquer les tentatives d'infrastructure",
985
990
  "executionHistory": "Historique d'exécution",
986
991
  "hideExecutionHistory": "Masquer l'historique d'exécution",
992
+ "attemptSucceeded": "Réussi",
993
+ "outputTruncated": "Sortie tronquée pour garder l'historique d'exécution compact.",
987
994
  "editingConclusions": "Modification des conclusions",
988
995
  "editConclusionsPlaceholder": "Modifiez les conclusions de l'agent ; vos modifications sont enregistrées lorsque vous approuvez…",
989
996
  "noProseOutput": "Cet agent n'a produit aucune sortie en texte libre.",
@@ -2193,11 +2200,22 @@
2193
2200
  },
2194
2201
  "budget": {
2195
2202
  "heading": "Budget de dépenses mensuel",
2196
- "body": "L'utilisation des jetons est décomptée par appel LLM, valorisée et plafonnée par ce budget. Une fois atteint, les exécutions de cet espace de travail sont mises en pause et le tableau affiche un avertissement. Laissez vide pour hériter de la valeur par défaut intégrée (environ 100 EUR/mois).",
2203
+ "body": "L'utilisation des jetons est décomptée par appel LLM, valorisée et plafonnée par ces budgets. Une exécution est mise en pause dès qu'un niveau auquel elle appartient est épuisé. Laissez un champ vide pour ne fixer aucune limite à ce niveau (le niveau de l'espace de travail hérite alors de la valeur par défaut intégrée, environ 100 EUR/mois).",
2197
2204
  "monthlyLimit": "Limite mensuelle",
2198
2205
  "defaultPlaceholder": "Par défaut",
2199
2206
  "currency": "Devise (ISO 4217)",
2200
- "save": "Enregistrer le budget"
2207
+ "save": "Enregistrer le budget",
2208
+ "saveTier": "Enregistrer",
2209
+ "workspace": "Cet espace de travail",
2210
+ "account": "Compte (tous les espaces de travail)",
2211
+ "user": "Vous (toutes vos exécutions)",
2212
+ "accountBody": "Un plafond pour tous les espaces de travail de ce compte.",
2213
+ "userBody": "Un plafond pour toutes les exécutions que vous lancez, dans n'importe quel espace de travail.",
2214
+ "hardCap": "Limite de l'opérateur : {amount}",
2215
+ "hardCapHint": "définie par le déploiement ; la valeur ne peut pas la dépasser",
2216
+ "spent": "{spent} sur {limit} dépensé ce mois-ci",
2217
+ "adminOnly": "Seul un administrateur du compte peut modifier le budget du compte.",
2218
+ "noLimitPlaceholder": "Aucune limite"
2201
2219
  },
2202
2220
  "toast": {
2203
2221
  "saved": "Paramètres enregistrés",
@@ -688,7 +688,12 @@
688
688
  "urlSource": "מקור כתובת הסביבה המוצע: {source}. המטפל של המרחב שולט בכך; הגדר זאת שם.",
689
689
  "namespace": "המניפסטים מקבעים את מרחב השמות \"{namespace}\"; מומלץ לכבד אותו במטפל של המרחב.",
690
690
  "confidenceHigh": "זוהה",
691
- "confidenceLow": "הצעה"
691
+ "confidenceLow": "הצעה",
692
+ "unavailable": {
693
+ "title": "סביבות זמניות אינן מופעלות",
694
+ "body": "הזיהוי האוטומטי קורא מאגר זה כדי להציע תצורת סביבת בדיקה (Kubernetes או Docker Compose), אך שילוב הסביבות הזמניות מכובה בפריסה זו. זה נפרד מחיבור ה-GitHub שלך. מי שמפעיל את השרת צריך להפעיל אותו (להגדיר את ENVIRONMENTS_ENABLED ומפתח הצפנה); לאחר מכן הזיהוי האוטומטי וההקצאה יהיו זמינים.",
695
+ "docs": "כיצד להפעיל סביבות זמניות"
696
+ }
692
697
  },
693
698
  "customManifestPathHint": "מתמלא מברירת המחדל של הסוג בעת הבחירה. השתמש ב'זיהוי' כדי לאתר מניפסט קיים במאגר.",
694
699
  "generateManifest": {
@@ -984,6 +989,8 @@
984
989
  "hideInfraAttempts": "הסתר ניסיונות תשתית",
985
990
  "executionHistory": "היסטוריית הרצה",
986
991
  "hideExecutionHistory": "הסתר היסטוריית הרצה",
992
+ "attemptSucceeded": "הצליח",
993
+ "outputTruncated": "הפלט נקטע כדי לשמור על היסטוריית ההרצה קומפקטית.",
987
994
  "editingConclusions": "עריכת המסקנות",
988
995
  "editConclusionsPlaceholder": "ערוך את מסקנות הסוכן; העריכות שלך נשמרות כשתאשר…",
989
996
  "noProseOutput": "סוכן זה לא הפיק פלט טקסטואלי.",
@@ -2314,11 +2321,22 @@
2314
2321
  },
2315
2322
  "budget": {
2316
2323
  "heading": "תקציב הוצאה חודשי",
2317
- "body": "שימוש באסימונים נמדד לכל קריאת LLM, מתומחר ומוגבל על ידי תקציב זה. כשמגיעים אליו, הרצות בסביבת עבודה זו מושהות והלוח מציג אזהרה. השאר ריק כדי לרשת את ברירת המחדל המובנית (כ-100 EUR לחודש).",
2324
+ "body": "שימוש באסימונים נמדד לכל קריאת LLM, מתומחר ומוגבל על ידי תקציבים אלה. הרצה מושהית כאשר כל שכבה שאליה היא שייכת מוצתה. השאר שדה ריק כדי לא להגביל את אותה שכבה (שכבת סביבת העבודה יורשת אז את ברירת המחדל המובנית, כ-100 EUR לחודש).",
2318
2325
  "monthlyLimit": "מגבלה חודשית",
2319
2326
  "defaultPlaceholder": "ברירת מחדל",
2320
2327
  "currency": "מטבע (ISO 4217)",
2321
- "save": "שמור תקציב"
2328
+ "save": "שמור תקציב",
2329
+ "saveTier": "שמור",
2330
+ "workspace": "סביבת עבודה זו",
2331
+ "account": "חשבון (כל סביבות העבודה)",
2332
+ "user": "אתה (כל ההרצות שלך)",
2333
+ "accountBody": "תקרה לכל סביבות העבודה בחשבון זה.",
2334
+ "userBody": "תקרה לכל הרצה שאתה מתחיל, בכל סביבת עבודה.",
2335
+ "hardCap": "מגבלת מפעיל: {amount}",
2336
+ "hardCapHint": "נקבעת על ידי הפריסה; הערך אינו יכול לחרוג ממנה",
2337
+ "spent": "{spent} מתוך {limit} הוצאו החודש",
2338
+ "adminOnly": "רק מנהל חשבון יכול לשנות את תקציב החשבון.",
2339
+ "noLimitPlaceholder": "ללא הגבלה"
2322
2340
  },
2323
2341
  "toast": {
2324
2342
  "saved": "ההגדרות נשמרו",
@@ -688,7 +688,12 @@
688
688
  "urlSource": "推奨される環境 URL ソース: {source}。これはワークスペースのハンドラーが管理します。そちらで設定してください。",
689
689
  "namespace": "マニフェストは名前空間「{namespace}」を固定しています。ワークスペースのハンドラーでそれを尊重することを推奨します。",
690
690
  "confidenceHigh": "検出",
691
- "confidenceLow": "提案"
691
+ "confidenceLow": "提案",
692
+ "unavailable": {
693
+ "title": "エフェメラル環境が有効になっていません",
694
+ "body": "自動検出はこのリポジトリを読み取ってテスト環境(Kubernetes または Docker Compose)の設定を提案しますが、このデプロイではエフェメラル環境統合が無効になっています。これは GitHub 接続とは別のものです。サーバーを運用している担当者が有効化(ENVIRONMENTS_ENABLED と暗号化キーを設定)すると、自動検出とプロビジョニングが利用できるようになります。",
695
+ "docs": "エフェメラル環境を有効にする方法"
696
+ }
692
697
  },
693
698
  "customManifestPathHint": "タイプを選択すると既定値が自動入力されます。リポジトリ内の既存のマニフェストを探すには「検出」を使用してください。",
694
699
  "generateManifest": {
@@ -984,6 +989,8 @@
984
989
  "hideInfraAttempts": "インフラの試行を非表示",
985
990
  "executionHistory": "実行履歴",
986
991
  "hideExecutionHistory": "実行履歴を非表示",
992
+ "attemptSucceeded": "成功",
993
+ "outputTruncated": "実行履歴を簡潔に保つため出力を切り詰めました。",
987
994
  "editingConclusions": "結論を編集中",
988
995
  "editConclusionsPlaceholder": "エージェントの結論を編集してください。編集内容は承認時に保存されます…",
989
996
  "noProseOutput": "このエージェントは文章出力を生成しませんでした。",
@@ -2315,11 +2322,22 @@
2315
2322
  },
2316
2323
  "budget": {
2317
2324
  "heading": "月間の支出予算",
2318
- "body": "トークン使用量は LLM 呼び出しごとに計測され、価格付けされ、この予算で制限されます。上限に達すると、このワークスペースの実行は一時停止し、ボードに警告が表示されます。空欄のままにすると組み込みのデフォルト (月額約 100 EUR) を継承します。",
2325
+ "body": "トークン使用量は LLM 呼び出しごとに計測され、価格付けされ、これらの予算で制限されます。実行は、それが属するいずれかの階層が使い切られると一時停止します。その階層に上限を設けない場合はフィールドを空欄のままにします (ワークスペース階層は組み込みのデフォルト、月額約 100 EUR を継承します)",
2319
2326
  "monthlyLimit": "月間上限",
2320
2327
  "defaultPlaceholder": "デフォルト",
2321
2328
  "currency": "通貨 (ISO 4217)",
2322
- "save": "予算を保存"
2329
+ "save": "予算を保存",
2330
+ "saveTier": "保存",
2331
+ "workspace": "このワークスペース",
2332
+ "account": "アカウント (すべてのワークスペース)",
2333
+ "user": "あなた (あなたのすべての実行)",
2334
+ "accountBody": "このアカウント内のすべてのワークスペースにまたがる上限です。",
2335
+ "userBody": "任意のワークスペースであなたが開始するすべての実行にまたがる上限です。",
2336
+ "hardCap": "運用上限: {amount}",
2337
+ "hardCapHint": "デプロイによって設定されます。この値を超えることはできません",
2338
+ "spent": "今月は {limit} のうち {spent} を使用しました",
2339
+ "adminOnly": "アカウント予算を変更できるのはアカウント管理者のみです。",
2340
+ "noLimitPlaceholder": "上限なし"
2323
2341
  },
2324
2342
  "toast": {
2325
2343
  "saved": "設定を保存しました",
@@ -688,7 +688,12 @@
688
688
  "urlSource": "Sugerowane źródło adresu URL środowiska: {source}. Zarządza tym handler przestrzeni roboczej; ustaw to tam.",
689
689
  "namespace": "Manifesty ustalają przestrzeń nazw \"{namespace}\"; zaleca się jej przestrzeganie w handlerze przestrzeni roboczej.",
690
690
  "confidenceHigh": "Wykryto",
691
- "confidenceLow": "Sugestia"
691
+ "confidenceLow": "Sugestia",
692
+ "unavailable": {
693
+ "title": "Środowiska efemeryczne nie są włączone",
694
+ "body": "Automatyczne wykrywanie odczytuje to repozytorium, aby zaproponować konfigurację środowiska testowego (Kubernetes lub Docker Compose), ale integracja środowisk efemerycznych jest wyłączona dla tego wdrożenia. Jest to niezależne od połączenia z GitHub. Osoba zarządzająca serwerem musi ją włączyć (ustawić ENVIRONMENTS_ENABLED oraz klucz szyfrowania); wtedy automatyczne wykrywanie i udostępnianie staną się dostępne.",
695
+ "docs": "Jak włączyć środowiska efemeryczne"
696
+ }
692
697
  },
693
698
  "customManifestPathHint": "Wypełniane wartością domyślną typu po jego wybraniu. Użyj Wykryj, aby znaleźć istniejący manifest w repozytorium.",
694
699
  "generateManifest": {
@@ -984,6 +989,8 @@
984
989
  "hideInfraAttempts": "Ukryj próby infrastrukturalne",
985
990
  "executionHistory": "Historia wykonania",
986
991
  "hideExecutionHistory": "Ukryj historię wykonania",
992
+ "attemptSucceeded": "Powodzenie",
993
+ "outputTruncated": "Wynik przycięty, aby zachować zwięzłość historii wykonania.",
987
994
  "editingConclusions": "Edytowanie wniosków",
988
995
  "editConclusionsPlaceholder": "Edytuj wnioski agenta; Twoje zmiany zostaną zapisane po zatwierdzeniu…",
989
996
  "noProseOutput": "Ten agent nie wytworzył wyniku tekstowego.",
@@ -2193,11 +2200,22 @@
2193
2200
  },
2194
2201
  "budget": {
2195
2202
  "heading": "Miesięczny budżet wydatków",
2196
- "body": "Zużycie tokenów jest mierzone na wywołanie LLM, wyceniane i ograniczane tym budżetem. Po jego osiągnięciu uruchomienia w tym obszarze roboczym są wstrzymywane, a tablica pokazuje ostrzeżenie. Pozostaw puste, aby odziedziczyć wbudowaną wartość domyślną (około 100 EUR/miesiąc).",
2203
+ "body": "Zużycie tokenów jest mierzone na wywołanie LLM, wyceniane i ograniczane tymi budżetami. Uruchomienie jest wstrzymywane, gdy dowolny poziom, do którego należy, zostanie wyczerpany. Pozostaw pole puste, aby nie ustawiać limitu dla tego poziomu (poziom obszaru roboczego dziedziczy wtedy wbudowaną wartość domyślną, około 100 EUR/miesiąc).",
2197
2204
  "monthlyLimit": "Limit miesięczny",
2198
2205
  "defaultPlaceholder": "Domyślny",
2199
2206
  "currency": "Waluta (ISO 4217)",
2200
- "save": "Zapisz budżet"
2207
+ "save": "Zapisz budżet",
2208
+ "saveTier": "Zapisz",
2209
+ "workspace": "Ten obszar roboczy",
2210
+ "account": "Konto (wszystkie obszary robocze)",
2211
+ "user": "Ty (wszystkie Twoje uruchomienia)",
2212
+ "accountBody": "Pułap dla wszystkich obszarów roboczych na tym koncie.",
2213
+ "userBody": "Pułap dla każdego uruchomienia, które rozpoczniesz, w dowolnym obszarze roboczym.",
2214
+ "hardCap": "Limit operatora: {amount}",
2215
+ "hardCapHint": "ustawiony przez wdrożenie; wartość nie może go przekroczyć",
2216
+ "spent": "Wydano {spent} z {limit} w tym miesiącu",
2217
+ "adminOnly": "Tylko administrator konta może zmienić budżet konta.",
2218
+ "noLimitPlaceholder": "Bez limitu"
2201
2219
  },
2202
2220
  "toast": {
2203
2221
  "saved": "Ustawienia zapisane",
@@ -688,7 +688,12 @@
688
688
  "urlSource": "Önerilen ortam URL kaynağı: {source}. Bunu çalışma alanı işleyicisi yönetir; oradan ayarlayın.",
689
689
  "namespace": "Manifestler \"{namespace}\" ad alanını sabitliyor; çalışma alanı işleyicisinde buna uymanız önerilir.",
690
690
  "confidenceHigh": "Algılandı",
691
- "confidenceLow": "Öneri"
691
+ "confidenceLow": "Öneri",
692
+ "unavailable": {
693
+ "title": "Geçici ortamlar etkin değil",
694
+ "body": "Otomatik algılama, bir test ortamı (Kubernetes veya Docker Compose) yapılandırması önermek için bu depoyu okur, ancak geçici ortam entegrasyonu bu dağıtımda kapalıdır. Bu, GitHub bağlantınızdan ayrıdır. Sunucuyu çalıştıran kişi bunu etkinleştirmelidir (ENVIRONMENTS_ENABLED ve bir şifreleme anahtarı ayarlayın); ardından otomatik algılama ve sağlama kullanılabilir hale gelir.",
695
+ "docs": "Geçici ortamlar nasıl etkinleştirilir"
696
+ }
692
697
  },
693
698
  "customManifestPathHint": "Türü seçtiğinizde varsayılan değeriyle doldurulur. Depodaki mevcut bir manifesti bulmak için Algıla'yı kullanın.",
694
699
  "generateManifest": {
@@ -984,6 +989,8 @@
984
989
  "hideInfraAttempts": "Altyapı denemelerini gizle",
985
990
  "executionHistory": "Yürütme geçmişi",
986
991
  "hideExecutionHistory": "Yürütme geçmişini gizle",
992
+ "attemptSucceeded": "Başarılı",
993
+ "outputTruncated": "Yürütme geçmişini derli toplu tutmak için çıktı kırpıldı.",
987
994
  "editingConclusions": "Sonuçlar düzenleniyor",
988
995
  "editConclusionsPlaceholder": "Aracının sonuçlarını düzenleyin; düzenlemeleriniz onayladığınızda kaydedilir…",
989
996
  "noProseOutput": "Bu aracı herhangi bir metin çıktısı üretmedi.",
@@ -2315,11 +2322,22 @@
2315
2322
  },
2316
2323
  "budget": {
2317
2324
  "heading": "Aylık harcama bütçesi",
2318
- "body": "Token kullanımı her LLM çağrısı için ölçülür, fiyatlandırılır ve bu bütçeyle sınırlandırılır. Ulaşıldığında bu çalışma alanındaki çalıştırmalar duraklatılır ve panoda bir uyarı gösterilir. Yerleşik varsayılanı (aylık yaklaşık 100 EUR) devralmak için boş bırakın.",
2325
+ "body": "Token kullanımı her LLM çağrısı için ölçülür, fiyatlandırılır ve bu bütçelerle sınırlandırılır. Bir çalıştırma, ait olduğu herhangi bir katman tükendiğinde duraklatılır. O katmanda sınır olmaması için bir alanı boş bırakın (çalışma alanı katmanı bu durumda yerleşik varsayılanı, aylık yaklaşık 100 EUR devralır).",
2319
2326
  "monthlyLimit": "Aylık sınır",
2320
2327
  "defaultPlaceholder": "Varsayılan",
2321
2328
  "currency": "Para birimi (ISO 4217)",
2322
- "save": "Bütçeyi kaydet"
2329
+ "save": "Bütçeyi kaydet",
2330
+ "saveTier": "Kaydet",
2331
+ "workspace": "Bu çalışma alanı",
2332
+ "account": "Hesap (tüm çalışma alanları)",
2333
+ "user": "Siz (başlattığınız tüm çalıştırmalar)",
2334
+ "accountBody": "Bu hesaptaki tüm çalışma alanlarını kapsayan bir üst sınır.",
2335
+ "userBody": "Herhangi bir çalışma alanında başlattığınız her çalıştırmayı kapsayan bir üst sınır.",
2336
+ "hardCap": "Operatör sınırı: {amount}",
2337
+ "hardCapHint": "dağıtım tarafından belirlenir; değer bunu aşamaz",
2338
+ "spent": "Bu ay {limit} bütçenin {spent} kadarı harcandı",
2339
+ "adminOnly": "Yalnızca bir hesap yöneticisi hesap bütçesini değiştirebilir.",
2340
+ "noLimitPlaceholder": "Sınır yok"
2323
2341
  },
2324
2342
  "toast": {
2325
2343
  "saved": "Ayarlar kaydedildi",
@@ -688,7 +688,12 @@
688
688
  "urlSource": "Запропоноване джерело URL середовища: {source}. Цим керує обробник робочого простору; налаштуйте його там.",
689
689
  "namespace": "Маніфести фіксують простір імен \"{namespace}\"; рекомендуємо дотримуватися його в обробнику робочого простору.",
690
690
  "confidenceHigh": "Виявлено",
691
- "confidenceLow": "Пропозиція"
691
+ "confidenceLow": "Пропозиція",
692
+ "unavailable": {
693
+ "title": "Ефемерні середовища не ввімкнено",
694
+ "body": "Автоматичне визначення читає цей репозиторій, щоб запропонувати конфігурацію тестового середовища (Kubernetes або Docker Compose), але інтеграцію ефемерних середовищ вимкнено для цього розгортання. Це окремо від вашого підключення до GitHub. Той, хто керує сервером, має ввімкнути її (задати ENVIRONMENTS_ENABLED і ключ шифрування); після цього автоматичне визначення та провізіонування стануть доступними.",
695
+ "docs": "Як увімкнути ефемерні середовища"
696
+ }
692
697
  },
693
698
  "customManifestPathHint": "Заповнюється значенням типу за замовчуванням під час вибору. Скористайтеся «Виявити», щоб знайти наявний маніфест у репозиторії.",
694
699
  "generateManifest": {
@@ -984,6 +989,8 @@
984
989
  "hideInfraAttempts": "Приховати спроби інфраструктури",
985
990
  "executionHistory": "Історія виконання",
986
991
  "hideExecutionHistory": "Приховати історію виконання",
992
+ "attemptSucceeded": "Успішно",
993
+ "outputTruncated": "Вивід обрізано, щоб історія виконання залишалася компактною.",
987
994
  "editingConclusions": "Редагування висновків",
988
995
  "editConclusionsPlaceholder": "Відредагуйте висновки агента; ваші зміни зберігаються після затвердження…",
989
996
  "noProseOutput": "Цей агент не створив текстового виводу.",
@@ -2193,11 +2200,22 @@
2193
2200
  },
2194
2201
  "budget": {
2195
2202
  "heading": "Місячний бюджет витрат",
2196
- "body": "Використання токенів вимірюється за кожен виклик LLM, оцінюється та обмежується цим бюджетом. Після його досягнення запуски в цьому робочому просторі призупиняються, а на дошці з'являється попередження. Залиште порожнім, щоб успадкувати вбудоване значення за замовчуванням (близько 100 EUR/місяць).",
2203
+ "body": "Використання токенів вимірюється за кожен виклик LLM, оцінюється та обмежується цими бюджетами. Запуск призупиняється, коли вичерпано будь-який рівень, до якого він належить. Залиште поле порожнім, щоб не встановлювати ліміт для цього рівня (рівень робочого простору тоді успадковує вбудоване значення за замовчуванням, близько 100 EUR/місяць).",
2197
2204
  "monthlyLimit": "Місячний ліміт",
2198
2205
  "defaultPlaceholder": "За замовчуванням",
2199
2206
  "currency": "Валюта (ISO 4217)",
2200
- "save": "Зберегти бюджет"
2207
+ "save": "Зберегти бюджет",
2208
+ "saveTier": "Зберегти",
2209
+ "workspace": "Цей робочий простір",
2210
+ "account": "Обліковий запис (усі робочі простори)",
2211
+ "user": "Ви (усі ваші запуски)",
2212
+ "accountBody": "Стеля для всіх робочих просторів у цьому обліковому записі.",
2213
+ "userBody": "Стеля для кожного запуску, який ви розпочинаєте, у будь-якому робочому просторі.",
2214
+ "hardCap": "Ліміт оператора: {amount}",
2215
+ "hardCapHint": "встановлюється розгортанням; значення не може його перевищити",
2216
+ "spent": "Цього місяця витрачено {spent} з {limit}",
2217
+ "adminOnly": "Лише адміністратор облікового запису може змінити бюджет облікового запису.",
2218
+ "noLimitPlaceholder": "Без обмеження"
2201
2219
  },
2202
2220
  "toast": {
2203
2221
  "saved": "Налаштування збережено",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cat-factory/app",
3
- "version": "0.98.0",
3
+ "version": "0.100.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.108.1"
37
+ "@cat-factory/contracts": "0.110.0"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@toad-contracts/testing": "0.3.2",