@cat-factory/app 0.46.10 → 0.46.12

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 (48) hide show
  1. package/app/components/auth/AuthGate.vue +2 -1
  2. package/app/components/auth/LoginScreen.vue +41 -31
  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 +75 -32
  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/observability/StepMetricsBar.vue +31 -11
  20. package/app/components/observability/StepModelActivity.vue +3 -2
  21. package/app/components/panels/AgentStepDetail.vue +52 -39
  22. package/app/components/panels/DecisionModal.vue +12 -5
  23. package/app/components/panels/GenericStructuredResultView.vue +16 -6
  24. package/app/components/panels/InspectorPanel.vue +35 -28
  25. package/app/components/panels/ObservabilityPanel.vue +92 -49
  26. package/app/components/panels/StepMetadataCard.vue +87 -30
  27. package/app/components/panels/StepRestartControl.vue +4 -3
  28. package/app/components/panels/StepRunMeta.vue +23 -10
  29. package/app/components/panels/StepTestReport.vue +14 -11
  30. package/app/components/panels/inspector/ContainerSummary.vue +25 -14
  31. package/app/components/panels/inspector/EpicChildren.vue +7 -4
  32. package/app/components/panels/inspector/RecurringScheduleSettings.vue +60 -19
  33. package/app/components/panels/inspector/ServiceFragments.vue +3 -2
  34. package/app/components/panels/inspector/ServiceReleaseHealthConfig.vue +18 -13
  35. package/app/components/panels/inspector/ServiceTestConfig.vue +50 -39
  36. package/app/components/panels/inspector/TaskAgentConfig.vue +6 -5
  37. package/app/components/panels/inspector/TaskDependencies.vue +9 -4
  38. package/app/components/panels/inspector/TaskEstimateBadge.vue +14 -13
  39. package/app/components/panels/inspector/TaskExecution.vue +65 -31
  40. package/app/components/panels/inspector/TaskRunSettings.vue +93 -56
  41. package/app/components/panels/inspector/TaskStructure.vue +5 -4
  42. package/app/components/providers/PersonalSubscriptionSection.vue +1 -2
  43. package/i18n/locales/en.json +814 -0
  44. package/i18n/locales/es.json +811 -0
  45. package/i18n/locales/fr.json +811 -0
  46. package/i18n/locales/pl.json +811 -0
  47. package/i18n/locales/uk.json +811 -0
  48. package/package.json +1 -1
@@ -7,44 +7,72 @@ import type { Notification } from '~/types/domain'
7
7
  // (merge / confirm / retry) or dismissed. Hydrated from the snapshot and patched
8
8
  // live via the `notification` WorkspaceEvent.
9
9
 
10
+ const { t, te } = useI18n()
11
+
10
12
  const notifications = useNotificationsStore()
11
13
  const ui = useUiStore()
12
14
  const execution = useExecutionStore()
13
15
 
14
16
  const busy = ref<string | null>(null)
15
17
 
16
- /** Per-type display metadata (icon, colour, primary-action label). */
18
+ /** Per-type display metadata (icon, colour). The primary-action label is resolved
19
+ * separately through the i18n catalog (`ACTION_KEYS`). */
17
20
  type Accent = 'warning' | 'primary' | 'error'
18
- const META: Record<Notification['type'], { icon: string; color: Accent; action: string }> = {
19
- merge_review: { icon: 'i-lucide-git-pull-request-arrow', color: 'warning', action: 'Merge' },
20
- pipeline_complete: { icon: 'i-lucide-circle-check', color: 'primary', action: 'Confirm & merge' },
21
- ci_failed: { icon: 'i-lucide-triangle-alert', color: 'error', action: 'Retry run' },
22
- test_failed: { icon: 'i-lucide-flask-conical', color: 'error', action: 'Retry run' },
21
+ const META: Record<Notification['type'], { icon: string; color: Accent }> = {
22
+ merge_review: { icon: 'i-lucide-git-pull-request-arrow', color: 'warning' },
23
+ pipeline_complete: { icon: 'i-lucide-circle-check', color: 'primary' },
24
+ ci_failed: { icon: 'i-lucide-triangle-alert', color: 'error' },
25
+ test_failed: { icon: 'i-lucide-flask-conical', color: 'error' },
23
26
  // Clicking the title opens the review window for the task (see `reveal`); "act" just marks
24
27
  // it read (the server performs no side-effect for this type).
25
- requirement_review: { icon: 'i-lucide-clipboard-list', color: 'primary', action: 'Mark read' },
28
+ requirement_review: { icon: 'i-lucide-clipboard-list', color: 'primary' },
26
29
  // Clicking the title opens the clarity review window for the task (see `reveal`); "act"
27
30
  // just marks it read (the server performs no side-effect for this type).
28
- clarity_review: { icon: 'i-lucide-bug', color: 'primary', action: 'Mark read' },
31
+ clarity_review: { icon: 'i-lucide-bug', color: 'primary' },
29
32
  // A post-release Datadog regression the on-call agent investigated. The human decides
30
33
  // whether to revert (in GitHub via the PR link) or acknowledge; "act" marks it handled.
31
- release_regression: { icon: 'i-lucide-activity', color: 'error', action: 'Acknowledge' },
34
+ release_regression: { icon: 'i-lucide-activity', color: 'error' },
32
35
  // Clicking the title opens the parked step's decision surface (companion → step detail
33
36
  // with the iteration-cap prompt; requirements → the review window); "act" just marks it
34
37
  // read (the decision itself is resolved in that surface, not here).
35
- decision_required: { icon: 'i-lucide-circle-help', color: 'warning', action: 'Mark read' },
38
+ decision_required: { icon: 'i-lucide-circle-help', color: 'warning' },
36
39
  // Clicking the title opens the human-testing window for the task (see `reveal`); "act" just
37
40
  // marks it read (the gate is resolved in that window — confirm / request a fix — not here).
38
- human_test_ready: { icon: 'i-lucide-user-check', color: 'primary', action: 'Mark read' },
41
+ human_test_ready: { icon: 'i-lucide-user-check', color: 'primary' },
39
42
  // Clicking the title opens the visual-confirmation window for the task (see `reveal`); "act"
40
43
  // just marks it read (the gate is resolved in that window — approve / request a fix — not here).
41
- visual_confirmation_ready: { icon: 'i-lucide-camera', color: 'primary', action: 'Mark read' },
44
+ visual_confirmation_ready: { icon: 'i-lucide-camera', color: 'primary' },
42
45
  // Clicking the title opens the task's gate window (where the human can request a freeform
43
46
  // fix); "act" just marks it read (approval happens on GitHub, not here).
44
- human_review: { icon: 'i-lucide-users', color: 'primary', action: 'Mark read' },
47
+ human_review: { icon: 'i-lucide-users', color: 'primary' },
45
48
  // Clicking the title opens the Follow-up companion window for the run (see `reveal`); "act"
46
49
  // just marks it read (items are decided in that window — file / send back / answer — not here).
47
- followup_pending: { icon: 'i-lucide-compass', color: 'warning', action: 'Mark read' },
50
+ followup_pending: { icon: 'i-lucide-compass', color: 'warning' },
51
+ }
52
+
53
+ // Per-type primary-action label. An exhaustive Record keyed off the notification
54
+ // type union (a missing member fails the typecheck); each value is a LITERAL catalog
55
+ // key so the typed-message-keys check sees it. Leaf keys mirror the enum value verbatim.
56
+ const ACTION_KEYS: Record<Notification['type'], string> = {
57
+ merge_review: 'layout.notifications.action.merge_review',
58
+ pipeline_complete: 'layout.notifications.action.pipeline_complete',
59
+ ci_failed: 'layout.notifications.action.ci_failed',
60
+ test_failed: 'layout.notifications.action.test_failed',
61
+ requirement_review: 'layout.notifications.action.requirement_review',
62
+ clarity_review: 'layout.notifications.action.clarity_review',
63
+ release_regression: 'layout.notifications.action.release_regression',
64
+ decision_required: 'layout.notifications.action.decision_required',
65
+ human_test_ready: 'layout.notifications.action.human_test_ready',
66
+ visual_confirmation_ready: 'layout.notifications.action.visual_confirmation_ready',
67
+ human_review: 'layout.notifications.action.human_review',
68
+ followup_pending: 'layout.notifications.action.followup_pending',
69
+ }
70
+
71
+ /** The localized primary-action label for a notification (te()-guarded against a
72
+ * locale that omits the key, falling back to the generic "mark read" verb). */
73
+ function actionLabel(n: Notification): string {
74
+ const key = ACTION_KEYS[n.type]
75
+ return te(key) ? t(key) : t('layout.notifications.action.markRead')
48
76
  }
49
77
 
50
78
  /** A notification the escalation sweep has flagged as overdue (waited past the threshold). */
@@ -176,7 +204,7 @@ function revealDecision(n: Notification) {
176
204
  <template #content>
177
205
  <div class="max-h-[28rem] w-[min(24rem,92vw)] overflow-y-auto p-2">
178
206
  <div class="px-2 py-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
179
- Needs your attention
207
+ {{ t('layout.notifications.heading') }}
180
208
  </div>
181
209
  <div
182
210
  v-for="n in notifications.open"
@@ -208,7 +236,7 @@ function revealDecision(n: Notification) {
208
236
  v-if="isUrgent(n)"
209
237
  class="shrink-0 rounded bg-error-500/20 px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-error-400"
210
238
  >
211
- Overdue
239
+ {{ t('layout.notifications.overdue') }}
212
240
  </span>
213
241
  </div>
214
242
  <p class="mt-0.5 text-[11px] leading-snug text-slate-400">{{ n.body }}</p>
@@ -219,7 +247,8 @@ function revealDecision(n: Notification) {
219
247
  rel="noopener"
220
248
  class="mt-1 inline-flex items-center gap-1 text-[11px] text-sky-400 hover:underline"
221
249
  >
222
- <UIcon name="i-lucide-external-link" class="h-3 w-3" /> Open PR
250
+ <UIcon name="i-lucide-external-link" class="h-3 w-3" />
251
+ {{ t('layout.notifications.openPr') }}
223
252
  </a>
224
253
  <div class="mt-2 flex items-center gap-1.5">
225
254
  <UButton
@@ -230,7 +259,7 @@ function revealDecision(n: Notification) {
230
259
  :loading="busy === n.id"
231
260
  @click="act(n)"
232
261
  >
233
- {{ META[n.type].action }}
262
+ {{ actionLabel(n) }}
234
263
  </UButton>
235
264
  <UButton
236
265
  data-testid="notification-dismiss"
@@ -240,7 +269,7 @@ function revealDecision(n: Notification) {
240
269
  :disabled="busy === n.id"
241
270
  @click="dismiss(n)"
242
271
  >
243
- Dismiss
272
+ {{ t('layout.notifications.dismiss') }}
244
273
  </UButton>
245
274
  </div>
246
275
  </div>
@@ -5,6 +5,7 @@
5
5
  // previously sat, confusingly, among the workspace-wide integrations. Each row reuses the
6
6
  // existing per-panel handlers via `ui.openFromPersonal(...)`, so opening one closes this
7
7
  // hub, reveals that panel, and gives it a "Back to My setup" control (IntegrationBackTitle).
8
+ const { t } = useI18n()
8
9
  const ui = useUiStore()
9
10
  const userSecrets = useUserSecretsStore()
10
11
  const localModels = useLocalModelsStore()
@@ -54,14 +55,14 @@ const groups = computed<PersonalGroup[]>(() => {
54
55
  // --- Source control --------------------------------------------------------
55
56
  const pat = !!userSecrets.statusFor('github_pat')
56
57
  out.push({
57
- title: 'Source control',
58
+ title: t('layout.personalSetup.sourceControl.title'),
58
59
  items: [
59
60
  {
60
61
  key: 'github-pat',
61
62
  icon: 'i-lucide-key-round',
62
- label: 'My GitHub token',
63
- description: 'A personal access token used for runs you start (pushes, PRs, CI, merge).',
64
- status: pat ? 'Connected' : undefined,
63
+ label: t('layout.personalSetup.githubToken.label'),
64
+ description: t('layout.personalSetup.githubToken.description'),
65
+ status: pat ? t('layout.personalSetup.connected') : undefined,
65
66
  connected: pat,
66
67
  onClick: () => go(ui.openUserSecrets),
67
68
  },
@@ -72,24 +73,27 @@ const groups = computed<PersonalGroup[]>(() => {
72
73
  const runnerCount = localModels.endpoints.length
73
74
  const subCount = personalSubs.subscriptions.length
74
75
  out.push({
75
- title: 'Models',
76
+ title: t('layout.personalSetup.models.title'),
76
77
  items: [
77
78
  {
78
79
  key: 'local-runners',
79
80
  icon: 'i-lucide-server',
80
- label: 'My local runners',
81
- description: 'Your own-machine model runners (Ollama, LM Studio, vLLM…).',
82
- status: runnerCount ? `${runnerCount} connected` : undefined,
81
+ label: t('layout.personalSetup.localRunners.label'),
82
+ description: t('layout.personalSetup.localRunners.description'),
83
+ status: runnerCount
84
+ ? t('layout.personalSetup.connectedCount', { count: runnerCount }, runnerCount)
85
+ : undefined,
83
86
  connected: runnerCount > 0,
84
87
  onClick: () => go(ui.openLocalModels),
85
88
  },
86
89
  {
87
90
  key: 'personal-subs',
88
91
  icon: 'i-lucide-user',
89
- label: 'My subscriptions',
90
- description:
91
- 'Individual-usage plans you unlock with a personal password (Claude, GLM, Codex).',
92
- status: subCount ? `${subCount} connected` : undefined,
92
+ label: t('layout.personalSetup.subscriptions.label'),
93
+ description: t('layout.personalSetup.subscriptions.description'),
94
+ status: subCount
95
+ ? t('layout.personalSetup.connectedCount', { count: subCount }, subCount)
96
+ : undefined,
93
97
  connected: subCount > 0,
94
98
  onClick: () => go(() => ui.openVendorCredentials('personal')),
95
99
  },
@@ -101,11 +105,15 @@ const groups = computed<PersonalGroup[]>(() => {
101
105
  </script>
102
106
 
103
107
  <template>
104
- <UModal v-model:open="open" title="My setup" :ui="{ content: 'max-w-xl' }">
108
+ <UModal
109
+ v-model:open="open"
110
+ :title="t('layout.personalSetup.title')"
111
+ :ui="{ content: 'max-w-xl' }"
112
+ >
105
113
  <template #body>
106
114
  <div class="space-y-5">
107
115
  <p class="text-xs text-slate-400">
108
- Your personal connections — used for runs you start, and visible only to you.
116
+ {{ t('layout.personalSetup.intro') }}
109
117
  </p>
110
118
 
111
119
  <section v-for="group in groups" :key="group.title">
@@ -125,9 +133,11 @@ const groups = computed<PersonalGroup[]>(() => {
125
133
  <div class="flex items-center gap-2">
126
134
  <span class="truncate text-sm font-medium text-slate-100">{{ item.label }}</span>
127
135
  <UBadge v-if="item.connected" color="success" variant="subtle" size="sm">
128
- {{ item.status || 'Connected' }}
136
+ {{ item.status || t('layout.personalSetup.connected') }}
129
137
  </UBadge>
130
- <span v-else class="text-[11px] text-slate-500">Not connected</span>
138
+ <span v-else class="text-[11px] text-slate-500">
139
+ {{ t('layout.personalSetup.notConnected') }}
140
+ </span>
131
141
  </div>
132
142
  <p class="truncate text-xs text-slate-400">{{ item.description }}</p>
133
143
  </div>
@@ -7,13 +7,19 @@
7
7
  import { computed, ref, watch } from 'vue'
8
8
  import type { ProviderConnectionKind } from '~/types/providerConnections'
9
9
 
10
+ const { t } = useI18n()
10
11
  const ui = useUiStore()
11
12
  const workspace = useWorkspaceStore()
12
13
  const store = useProviderConnectionsStore()
13
14
 
14
- const LABEL: Record<ProviderConnectionKind, string> = {
15
- environment: 'Environment provider',
16
- 'runner-pool': 'Runner pool',
15
+ // Exhaustive per-kind title/action keys (leaf names mirror the kind verbatim).
16
+ const TITLE_KEYS: Record<ProviderConnectionKind, string> = {
17
+ environment: 'layout.providerConfigBanner.titleOne.environment',
18
+ 'runner-pool': 'layout.providerConfigBanner.titleOne.runner-pool',
19
+ }
20
+ const ACTION_KEYS: Record<ProviderConnectionKind, string> = {
21
+ environment: 'layout.providerConfigBanner.action.environment',
22
+ 'runner-pool': 'layout.providerConfigBanner.action.runner-pool',
17
23
  }
18
24
 
19
25
  // Probe both providers once the workspace is ready (the descriptor view is secret-less).
@@ -44,8 +50,8 @@ const show = computed(() => pending.value.length > 0 && !dismissed.value)
44
50
  <h2 class="text-lg font-semibold text-amber-100">
45
51
  {{
46
52
  pending.length > 1
47
- ? 'Providers need configuration'
48
- : `${LABEL[pending[0]!]} needs configuration`
53
+ ? t('layout.providerConfigBanner.titleMany')
54
+ : t(TITLE_KEYS[pending[0]!])
49
55
  }}
50
56
  </h2>
51
57
  <UButton
@@ -53,13 +59,12 @@ const show = computed(() => pending.value.length > 0 && !dismissed.value)
53
59
  variant="ghost"
54
60
  size="xs"
55
61
  icon="i-lucide-x"
56
- aria-label="Dismiss"
62
+ :aria-label="t('common.close')"
57
63
  @click="dismissed = true"
58
64
  />
59
65
  </div>
60
66
  <p class="mt-1 text-sm text-amber-200/90">
61
- A provider is wired for this instance but is missing required settings, so it can't
62
- run yet. Add the mandatory fields to finish connecting it.
67
+ {{ t('layout.providerConfigBanner.body') }}
63
68
  </p>
64
69
  <div class="mt-4 flex flex-wrap gap-2">
65
70
  <UButton
@@ -70,7 +75,7 @@ const show = computed(() => pending.value.length > 0 && !dismissed.value)
70
75
  icon="i-lucide-settings"
71
76
  @click="ui.openProviderConnection(k)"
72
77
  >
73
- Configure {{ LABEL[k].toLowerCase() }}
78
+ {{ t(ACTION_KEYS[k]) }}
74
79
  </UButton>
75
80
  </div>
76
81
  </div>
@@ -11,6 +11,8 @@ import LanguageSwitcher from '~/components/layout/LanguageSwitcher.vue'
11
11
  import UserMenu from '~/components/auth/UserMenu.vue'
12
12
  import { useViewport } from '~/composables/useViewport'
13
13
 
14
+ const { t } = useI18n()
15
+
14
16
  const documents = useDocumentsStore()
15
17
  const tasks = useTasksStore()
16
18
  const github = useGitHubStore()
@@ -107,7 +109,7 @@ watch(
107
109
  data-testid="sidebar-backdrop"
108
110
  role="button"
109
111
  tabindex="-1"
110
- :aria-label="$t('common.close')"
112
+ :aria-label="t('common.close')"
111
113
  @click="ui.closeMobileNav()"
112
114
  />
113
115
  </Transition>
@@ -118,7 +120,7 @@ watch(
118
120
  tabindex="-1"
119
121
  :role="drawerOpen ? 'dialog' : undefined"
120
122
  :aria-modal="drawerOpen ? 'true' : undefined"
121
- :aria-label="isCompact ? $t('nav.menu') : undefined"
123
+ :aria-label="isCompact ? t('nav.menu') : undefined"
122
124
  :inert="isCompact && !ui.mobileNavOpen"
123
125
  class="fixed inset-y-0 left-0 z-40 flex h-full w-64 shrink-0 flex-col gap-4 overflow-y-auto border-r border-slate-800 bg-slate-900/95 p-3 backdrop-blur transition-transform duration-200 focus:outline-none lg:static lg:z-auto lg:translate-x-0 lg:bg-slate-900/80"
124
126
  :class="ui.mobileNavOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'"
@@ -134,13 +136,13 @@ watch(
134
136
  @click="ui.openCommandBar()"
135
137
  >
136
138
  <UIcon name="i-lucide-search" class="h-4 w-4 shrink-0" />
137
- <span class="flex-1 truncate">{{ $t('nav.commandBar') }}</span>
139
+ <span class="flex-1 truncate">{{ t('nav.commandBar') }}</span>
138
140
  <UKbd value="⌘K" />
139
141
  </button>
140
142
 
141
143
  <section>
142
144
  <h2 class="mb-2 px-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
143
- {{ $t('nav.create') }}
145
+ {{ t('nav.create') }}
144
146
  </h2>
145
147
  <div class="space-y-1.5">
146
148
  <UButton
@@ -152,7 +154,7 @@ watch(
152
154
  class="justify-start"
153
155
  @click="ui.openBuilder()"
154
156
  >
155
- {{ $t('nav.buildPipeline') }}
157
+ {{ t('nav.buildPipeline') }}
156
158
  </UButton>
157
159
  </div>
158
160
  </section>
@@ -161,7 +163,7 @@ watch(
161
163
 
162
164
  <section>
163
165
  <h2 class="mb-2 px-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
164
- {{ $t('nav.repositories') }}
166
+ {{ t('nav.repositories') }}
165
167
  </h2>
166
168
  <div class="space-y-1.5">
167
169
  <UButton
@@ -174,7 +176,7 @@ watch(
174
176
  class="justify-start"
175
177
  @click="ui.openAddService()"
176
178
  >
177
- {{ $t('nav.addFromRepo') }}
179
+ {{ t('nav.addFromRepo') }}
178
180
  </UButton>
179
181
  <UButton
180
182
  block
@@ -185,7 +187,7 @@ watch(
185
187
  class="justify-start"
186
188
  @click="ui.openBootstrap()"
187
189
  >
188
- {{ $t('nav.bootstrapRepo') }}
190
+ {{ t('nav.bootstrapRepo') }}
189
191
  </UButton>
190
192
  </div>
191
193
  </section>
@@ -194,7 +196,7 @@ watch(
194
196
 
195
197
  <section>
196
198
  <h2 class="mb-2 px-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
197
- {{ $t('nav.integrations') }}
199
+ {{ t('nav.integrations') }}
198
200
  </h2>
199
201
  <div class="space-y-1.5">
200
202
  <!-- Every external system the workspace can enable/link now lives behind
@@ -209,7 +211,7 @@ watch(
209
211
  class="justify-start"
210
212
  @click="ui.openIntegrations()"
211
213
  >
212
- {{ $t('nav.integrations') }}
214
+ {{ t('nav.integrations') }}
213
215
  </UButton>
214
216
  <!-- The Sandbox: try prompt versions/models against graded fixtures, off to the
215
217
  side of the board. Opens the on-demand testing window. -->
@@ -222,7 +224,7 @@ watch(
222
224
  class="justify-start"
223
225
  @click="ui.openSandbox()"
224
226
  >
225
- {{ $t('nav.sandbox') }}
227
+ {{ t('nav.sandbox') }}
226
228
  </UButton>
227
229
  <!-- The Kaizen screen: grading history + verified prompt/agent/model combos. -->
228
230
  <UButton
@@ -234,7 +236,7 @@ watch(
234
236
  class="justify-start"
235
237
  @click="ui.openKaizen()"
236
238
  >
237
- {{ $t('nav.kaizen') }}
239
+ {{ t('nav.kaizen') }}
238
240
  </UButton>
239
241
  </div>
240
242
  </section>
@@ -243,7 +245,7 @@ watch(
243
245
  <USeparator />
244
246
  <section>
245
247
  <h2 class="mb-2 px-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
246
- {{ $t('nav.workspaceContext') }}
248
+ {{ t('nav.workspaceContext') }}
247
249
  </h2>
248
250
  <UButton
249
251
  block
@@ -254,7 +256,7 @@ watch(
254
256
  class="justify-start"
255
257
  @click="ui.openFragmentLibrary()"
256
258
  >
257
- {{ $t('nav.contextFragments') }}
259
+ {{ t('nav.contextFragments') }}
258
260
  </UButton>
259
261
  </section>
260
262
  </template>
@@ -262,7 +264,7 @@ watch(
262
264
  <USeparator />
263
265
  <section>
264
266
  <h2 class="mb-2 px-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
265
- {{ $t('nav.configuration') }}
267
+ {{ t('nav.configuration') }}
266
268
  </h2>
267
269
  <div class="space-y-1.5">
268
270
  <!-- Merge thresholds, issue writeback and default service best practices are
@@ -276,7 +278,7 @@ watch(
276
278
  class="justify-start"
277
279
  @click="ui.openWorkspaceSettings()"
278
280
  >
279
- {{ $t('nav.workspaceSettings') }}
281
+ {{ t('nav.workspaceSettings') }}
280
282
  </UButton>
281
283
  <UButton
282
284
  block
@@ -287,7 +289,7 @@ watch(
287
289
  class="justify-start"
288
290
  @click="ui.openModelConfig()"
289
291
  >
290
- {{ $t('nav.modelConfiguration') }}
292
+ {{ t('nav.modelConfiguration') }}
291
293
  </UButton>
292
294
  <!-- Account & team: members + roles, invitations, email sender, account API keys.
293
295
  Shown once accounts (auth) are enabled. -->
@@ -301,7 +303,7 @@ watch(
301
303
  class="justify-start"
302
304
  @click="ui.openAccountSettings()"
303
305
  >
304
- {{ $t('nav.accountSettings') }}
306
+ {{ t('nav.accountSettings') }}
305
307
  </UButton>
306
308
  </div>
307
309
  </section>
@@ -1,6 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import { computed } from 'vue'
3
3
 
4
+ const { t, n } = useI18n()
4
5
  const workspace = useWorkspaceStore()
5
6
 
6
7
  const spend = computed(() => workspace.spend)
@@ -8,18 +9,13 @@ const spend = computed(() => workspace.spend)
8
9
  const exceeded = computed(() => spend.value?.exceeded ?? false)
9
10
 
10
11
  function money(amount: number, currency: string) {
11
- try {
12
- return new Intl.NumberFormat(undefined, { style: 'currency', currency }).format(amount)
13
- } catch {
14
- // Fall back if the currency code isn't recognised by the runtime.
15
- return `${amount.toFixed(2)} ${currency}`
16
- }
12
+ return n(amount, { key: 'currency', currency })
17
13
  }
18
14
 
19
15
  const tokens = computed(() => {
20
16
  const s = spend.value
21
17
  if (!s) return ''
22
- return new Intl.NumberFormat().format(s.inputTokens + s.outputTokens)
18
+ return n(s.inputTokens + s.outputTokens, 'decimal')
23
19
  })
24
20
 
25
21
  const resuming = ref(false)
@@ -47,29 +43,33 @@ async function resume() {
47
43
  <UIcon name="i-lucide-octagon-alert" class="mt-0.5 h-10 w-10 shrink-0 text-red-400" />
48
44
  <div class="min-w-0 flex-1">
49
45
  <h2 class="text-lg font-semibold text-red-100">
50
- Spend limit reached — agent execution paused
46
+ {{ t('layout.spendWarningBanner.title') }}
51
47
  </h2>
52
48
  <p class="mt-1 text-sm text-red-200/90">
53
- This month's token spend has hit the configured budget, so running pipelines were
54
- paused to avoid further cost. Execution resumes automatically when the budget rolls
55
- over next month, or once the limit is raised.
49
+ {{ t('layout.spendWarningBanner.body') }}
56
50
  </p>
57
51
 
58
52
  <dl class="mt-4 grid grid-cols-2 gap-3 sm:grid-cols-3">
59
53
  <div class="rounded-lg bg-red-900/50 px-3 py-2">
60
- <dt class="text-[11px] uppercase tracking-wide text-red-300/80">Spent</dt>
54
+ <dt class="text-[11px] uppercase tracking-wide text-red-300/80">
55
+ {{ t('layout.spendWarningBanner.spent') }}
56
+ </dt>
61
57
  <dd class="text-base font-semibold tabular-nums text-red-50">
62
58
  {{ money(spend.costSpent, spend.currency) }}
63
59
  </dd>
64
60
  </div>
65
61
  <div class="rounded-lg bg-red-900/50 px-3 py-2">
66
- <dt class="text-[11px] uppercase tracking-wide text-red-300/80">Budget</dt>
62
+ <dt class="text-[11px] uppercase tracking-wide text-red-300/80">
63
+ {{ t('layout.spendWarningBanner.budget') }}
64
+ </dt>
67
65
  <dd class="text-base font-semibold tabular-nums text-red-50">
68
66
  {{ money(spend.costLimit, spend.currency) }}
69
67
  </dd>
70
68
  </div>
71
69
  <div class="rounded-lg bg-red-900/50 px-3 py-2">
72
- <dt class="text-[11px] uppercase tracking-wide text-red-300/80">Tokens</dt>
70
+ <dt class="text-[11px] uppercase tracking-wide text-red-300/80">
71
+ {{ t('layout.spendWarningBanner.tokens') }}
72
+ </dt>
73
73
  <dd class="text-base font-semibold tabular-nums text-red-50">{{ tokens }}</dd>
74
74
  </div>
75
75
  </dl>
@@ -82,10 +82,10 @@ async function resume() {
82
82
  :loading="resuming"
83
83
  @click="resume"
84
84
  >
85
- Resume anyway
85
+ {{ t('layout.spendWarningBanner.resume') }}
86
86
  </UButton>
87
87
  <span class="text-xs text-red-300/70">
88
- Resuming continues spending; it will pause again if still over budget.
88
+ {{ t('layout.spendWarningBanner.resumeHint') }}
89
89
  </span>
90
90
  </div>
91
91
  </div>
@@ -19,6 +19,8 @@ import {
19
19
  const props = defineProps<{ metrics: StepMetrics; clickable?: boolean }>()
20
20
  defineEmits<{ inspect: [] }>()
21
21
 
22
+ const { t } = useI18n()
23
+
22
24
  const m = computed(() => props.metrics)
23
25
  const headroom = computed(() => headroomRatio(m.value))
24
26
  const transport = computed(() => transportRatio(m.value))
@@ -38,24 +40,33 @@ const headroomTone = computed(() => headroomColor(headroom.value, m.value.trunca
38
40
  <!-- header line: call count + tokens + warning/error badges -->
39
41
  <div class="flex items-center gap-2">
40
42
  <UIcon name="i-lucide-activity" class="h-3.5 w-3.5 shrink-0 text-slate-500" />
41
- <span class="text-slate-300"> {{ m.calls }} {{ m.calls === 1 ? 'call' : 'calls' }} </span>
43
+ <span class="text-slate-300">
44
+ {{ t('observability.metricsBar.calls', { count: m.calls }, m.calls) }}
45
+ </span>
42
46
  <span class="text-slate-500">·</span>
43
- <span class="tabular-nums text-slate-400" title="Prompt / completion tokens">
47
+ <span
48
+ class="tabular-nums text-slate-400"
49
+ :title="t('observability.metricsBar.promptCompletionTokens')"
50
+ >
44
51
  {{ formatTokens(m.promptTokens) }}↑ {{ formatTokens(m.completionTokens) }}↓
45
52
  </span>
46
53
  <span
47
54
  v-if="(m.cachedPromptTokens ?? 0) > 0"
48
55
  class="tabular-nums text-emerald-400/80"
49
- title="Prompt tokens served from the provider's cache"
56
+ :title="t('observability.metricsBar.cachedTokensHint')"
50
57
  >
51
- ({{ formatTokens(m.cachedPromptTokens ?? 0) }} cached)
58
+ {{
59
+ t('observability.metricsBar.cachedTokens', {
60
+ tokens: formatTokens(m.cachedPromptTokens ?? 0),
61
+ })
62
+ }}
52
63
  </span>
53
64
  <div class="ml-auto flex items-center gap-1">
54
65
  <UBadge v-if="m.errors > 0" color="error" variant="subtle" size="sm">
55
- {{ m.errors }} error{{ m.errors === 1 ? '' : 's' }}
66
+ {{ t('observability.metricsBar.errors', { count: m.errors }, m.errors) }}
56
67
  </UBadge>
57
68
  <UBadge v-if="m.warnings > 0" color="warning" variant="subtle" size="sm">
58
- {{ m.warnings }} warning{{ m.warnings === 1 ? '' : 's' }}
69
+ {{ t('observability.metricsBar.warnings', { count: m.warnings }, m.warnings) }}
59
70
  </UBadge>
60
71
  <UIcon v-if="clickable" name="i-lucide-chevron-right" class="h-3.5 w-3.5 text-slate-600" />
61
72
  </div>
@@ -64,7 +75,7 @@ const headroomTone = computed(() => headroomColor(headroom.value, m.value.trunca
64
75
  <!-- output-limit headroom -->
65
76
  <div v-if="headroom !== null" class="mt-2">
66
77
  <div class="flex items-center justify-between text-[11px]">
67
- <span class="text-slate-500">Output limit</span>
78
+ <span class="text-slate-500">{{ t('observability.metricsBar.outputLimit') }}</span>
68
79
  <span class="tabular-nums" :class="headroomTone">
69
80
  {{ formatTokens(m.peakCompletionTokens) }} /
70
81
  {{ formatTokens(m.maxOutputTokens ?? 0) }} ({{ pct(headroom) }}%)
@@ -84,14 +95,20 @@ const headroomTone = computed(() => headroomColor(headroom.value, m.value.trunca
84
95
  />
85
96
  </div>
86
97
  <p v-if="m.truncatedCalls > 0" class="mt-1 text-[11px] text-rose-400">
87
- {{ m.truncatedCalls }} call{{ m.truncatedCalls === 1 ? '' : 's' }} truncated at the limit
98
+ {{
99
+ t(
100
+ 'observability.metricsBar.truncatedCalls',
101
+ { count: m.truncatedCalls },
102
+ m.truncatedCalls,
103
+ )
104
+ }}
88
105
  </p>
89
106
  </div>
90
107
 
91
108
  <!-- transport overhead vs model execution -->
92
109
  <div v-if="transport !== null" class="mt-2">
93
110
  <div class="flex items-center justify-between text-[11px]">
94
- <span class="text-slate-500">Transport vs execution</span>
111
+ <span class="text-slate-500">{{ t('observability.metricsBar.transportVsExecution') }}</span>
95
112
  <span class="tabular-nums text-slate-400">
96
113
  {{ formatMs(m.overheadMs) }} / {{ formatMs(m.upstreamMs) }}
97
114
  </span>
@@ -100,9 +117,12 @@ const headroomTone = computed(() => headroomColor(headroom.value, m.value.trunca
100
117
  <div
101
118
  class="h-full bg-sky-400/80"
102
119
  :style="{ width: `${pct(transport)}%` }"
103
- title="Transport / proxy overhead"
120
+ :title="t('observability.metricsBar.transportOverhead')"
121
+ />
122
+ <div
123
+ class="h-full bg-indigo-400/80 flex-1"
124
+ :title="t('observability.metricsBar.modelExecution')"
104
125
  />
105
- <div class="h-full bg-indigo-400/80 flex-1" title="Model execution" />
106
126
  </div>
107
127
  </div>
108
128
  </div>
@@ -18,6 +18,7 @@ const props = defineProps<{
18
18
  }>()
19
19
 
20
20
  const ui = useUiStore()
21
+ const { t } = useI18n()
21
22
  const hasCalls = computed(() => !!props.metrics && props.metrics.calls > 0)
22
23
 
23
24
  function openObservability() {
@@ -29,14 +30,14 @@ function openObservability() {
29
30
  <div v-if="instanceId || hasCalls">
30
31
  <div class="mb-1 flex items-center justify-between">
31
32
  <span class="text-[11px] font-semibold uppercase tracking-wide text-slate-500">
32
- Model activity
33
+ {{ t('observability.modelActivity') }}
33
34
  </span>
34
35
  <button
35
36
  v-if="instanceId"
36
37
  class="text-[11px] text-sky-400 hover:text-sky-300"
37
38
  @click="openObservability"
38
39
  >
39
- View all calls →
40
+ {{ t('observability.viewAllCalls') }}
40
41
  </button>
41
42
  </div>
42
43
  <StepMetricsBar