@cat-factory/app 0.54.0 → 0.54.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,15 +8,22 @@ import type { Recurrence } from '~/types/recurring'
8
8
  const props = defineProps<{ modelValue: Recurrence }>()
9
9
  const emit = defineEmits<{ 'update:modelValue': [Recurrence] }>()
10
10
 
11
- const WEEKDAYS = [
12
- { value: 0, label: 'Sun' },
13
- { value: 1, label: 'Mon' },
14
- { value: 2, label: 'Tue' },
15
- { value: 3, label: 'Wed' },
16
- { value: 4, label: 'Thu' },
17
- { value: 5, label: 'Fri' },
18
- { value: 6, label: 'Sat' },
19
- ]
11
+ const { t } = useI18n()
12
+
13
+ // Exhaustive day-index→label map of literal `t(...)` keys (keeps the typed-key drift
14
+ // guard live for this short-form weekday list).
15
+ const WEEKDAY_LABELS = computed<Record<number, string>>(() => ({
16
+ 0: t('recurring.weekday.sun'),
17
+ 1: t('recurring.weekday.mon'),
18
+ 2: t('recurring.weekday.tue'),
19
+ 3: t('recurring.weekday.wed'),
20
+ 4: t('recurring.weekday.thu'),
21
+ 5: t('recurring.weekday.fri'),
22
+ 6: t('recurring.weekday.sat'),
23
+ }))
24
+ const WEEKDAYS = computed(() =>
25
+ [0, 1, 2, 3, 4, 5, 6].map((value) => ({ value, label: WEEKDAY_LABELS.value[value]! })),
26
+ )
20
27
 
21
28
  function patch(p: Partial<Recurrence>) {
22
29
  emit('update:modelValue', { ...props.modelValue, ...p })
@@ -60,7 +67,7 @@ const timezoneOptions = computed(() =>
60
67
 
61
68
  <template>
62
69
  <div class="space-y-3">
63
- <UFormField label="Run every">
70
+ <UFormField :label="t('recurring.runEvery')">
64
71
  <div class="flex items-center gap-2">
65
72
  <UInput
66
73
  :model-value="modelValue.intervalHours"
@@ -69,11 +76,11 @@ const timezoneOptions = computed(() =>
69
76
  class="w-24"
70
77
  @update:model-value="patch({ intervalHours: Math.max(1, Number($event) || 1) })"
71
78
  />
72
- <span class="text-xs text-slate-400">hours</span>
79
+ <span class="text-xs text-slate-400">{{ t('recurring.hours') }}</span>
73
80
  </div>
74
81
  </UFormField>
75
82
 
76
- <UFormField label="Allowed days" help="Leave all off to run any day.">
83
+ <UFormField :label="t('recurring.allowedDays')" :help="t('recurring.allowedDaysHelp')">
77
84
  <div class="flex flex-wrap gap-1">
78
85
  <UButton
79
86
  v-for="d in WEEKDAYS"
@@ -91,7 +98,7 @@ const timezoneOptions = computed(() =>
91
98
  <UFormField>
92
99
  <UCheckbox
93
100
  :model-value="windowEnabled"
94
- label="Only within an hour-of-day window (e.g. business hours)"
101
+ :label="t('recurring.windowToggle')"
95
102
  @update:model-value="toggleWindow(Boolean($event))"
96
103
  />
97
104
  </UFormField>
@@ -103,7 +110,7 @@ const timezoneOptions = computed(() =>
103
110
  class="w-28"
104
111
  @update:model-value="patch({ windowStartHour: Number($event) })"
105
112
  />
106
- <span class="text-xs text-slate-400">to</span>
113
+ <span class="text-xs text-slate-400">{{ t('recurring.to') }}</span>
107
114
  <USelect
108
115
  :model-value="modelValue.windowEndHour ?? 24 % 24"
109
116
  :items="hours"
@@ -112,7 +119,7 @@ const timezoneOptions = computed(() =>
112
119
  />
113
120
  </div>
114
121
 
115
- <UFormField label="Timezone">
122
+ <UFormField :label="t('recurring.timezone')">
116
123
  <USelect
117
124
  :model-value="modelValue.timezone"
118
125
  :items="timezoneOptions"
@@ -6,11 +6,39 @@
6
6
  // and Fixtures (the graded inputs each run is scored against). Loaded on demand when the
7
7
  // window opens; 503 (the deployment hasn't provisioned the Sandbox DB) shows a notice.
8
8
  import { computed, ref, watch } from 'vue'
9
- import type { SandboxGrade, SandboxPromptVersion, SandboxRun } from '~/types/sandbox'
9
+ import type {
10
+ SandboxExperimentStatus,
11
+ SandboxFixtureKind,
12
+ SandboxGrade,
13
+ SandboxPromptVersion,
14
+ SandboxRun,
15
+ } from '~/types/sandbox'
10
16
 
11
17
  const ui = useUiStore()
12
18
  const store = useSandboxStore()
13
19
  const toast = useToast()
20
+ const { t } = useI18n()
21
+
22
+ // Exhaustive enum→label maps of literal `t(...)` keys (keeps the typed-key drift guard
23
+ // live for these runtime-indexed status/kind/origin lookups).
24
+ const EXPERIMENT_STATUS_LABEL = computed<Record<SandboxExperimentStatus, string>>(() => ({
25
+ draft: t('sandbox.experimentStatus.draft'),
26
+ running: t('sandbox.experimentStatus.running'),
27
+ done: t('sandbox.experimentStatus.done'),
28
+ failed: t('sandbox.experimentStatus.failed'),
29
+ }))
30
+ const FIXTURE_KIND_LABEL = computed<Record<SandboxFixtureKind, string>>(() => ({
31
+ requirements: t('sandbox.fixtureKind.requirements'),
32
+ clarity: t('sandbox.fixtureKind.clarity'),
33
+ architecture: t('sandbox.fixtureKind.architecture'),
34
+ 'code-review': t('sandbox.fixtureKind.code-review'),
35
+ 'repo-feature': t('sandbox.fixtureKind.repo-feature'),
36
+ 'repo-bug': t('sandbox.fixtureKind.repo-bug'),
37
+ }))
38
+ const FIXTURE_ORIGIN_LABEL = computed<Record<'builtin' | 'custom', string>>(() => ({
39
+ builtin: t('sandbox.fixtureOrigin.builtin'),
40
+ custom: t('sandbox.fixtureOrigin.custom'),
41
+ }))
14
42
 
15
43
  const open = computed({
16
44
  get: () => ui.sandboxOpen,
@@ -39,7 +67,7 @@ const selectedFixtureIds = ref<string[]>([])
39
67
  const selectedJudgeModel = ref<string>('')
40
68
 
41
69
  const judgeModelItems = computed(() => [
42
- { label: 'Deployment default', value: '' },
70
+ { label: t('sandbox.deploymentDefault'), value: '' },
43
71
  ...store.selectableModels.map((m) => ({ label: m.label, value: m.id })),
44
72
  ])
45
73
 
@@ -82,7 +110,7 @@ async function createAndRun() {
82
110
  if (!canRun.value) return
83
111
  try {
84
112
  const created = await store.createExperiment({
85
- name: name.value.trim() || `${agentKind.value} — sandbox run`,
113
+ name: name.value.trim() || t('sandbox.defaultRunName', { kind: agentKind.value }),
86
114
  agentKind: agentKind.value,
87
115
  judgeModel: selectedJudgeModel.value || undefined,
88
116
  matrix: {
@@ -92,12 +120,12 @@ async function createAndRun() {
92
120
  },
93
121
  })
94
122
  name.value = ''
95
- toast.add({ title: 'Running experiment…', icon: 'i-lucide-flask-conical', color: 'info' })
123
+ toast.add({ title: t('sandbox.toast.running'), icon: 'i-lucide-flask-conical', color: 'info' })
96
124
  await store.launch(created.id)
97
- toast.add({ title: 'Experiment complete', icon: 'i-lucide-check', color: 'success' })
125
+ toast.add({ title: t('sandbox.toast.complete'), icon: 'i-lucide-check', color: 'success' })
98
126
  } catch (e) {
99
127
  toast.add({
100
- title: 'Could not run the experiment',
128
+ title: t('sandbox.toast.runFailed'),
101
129
  description: e instanceof Error ? e.message : String(e),
102
130
  icon: 'i-lucide-triangle-alert',
103
131
  color: 'error',
@@ -149,11 +177,11 @@ async function saveVersion() {
149
177
  savingPrompt.value = true
150
178
  try {
151
179
  await store.saveVersion(editing.value.id, editText.value)
152
- toast.add({ title: 'Saved a new version', icon: 'i-lucide-check', color: 'success' })
180
+ toast.add({ title: t('sandbox.toast.versionSaved'), icon: 'i-lucide-check', color: 'success' })
153
181
  editing.value = null
154
182
  } catch (e) {
155
183
  toast.add({
156
- title: 'Could not save the version',
184
+ title: t('sandbox.toast.saveFailed'),
157
185
  description: e instanceof Error ? e.message : String(e),
158
186
  icon: 'i-lucide-triangle-alert',
159
187
  color: 'error',
@@ -169,7 +197,7 @@ async function archive(prompt: SandboxPromptVersion) {
169
197
  if (editing.value?.id === prompt.id) editing.value = null
170
198
  } catch (e) {
171
199
  toast.add({
172
- title: 'Could not archive',
200
+ title: t('sandbox.toast.archiveFailed'),
173
201
  description: e instanceof Error ? e.message : String(e),
174
202
  icon: 'i-lucide-triangle-alert',
175
203
  color: 'error',
@@ -181,8 +209,8 @@ async function archive(prompt: SandboxPromptVersion) {
181
209
  <template>
182
210
  <UModal
183
211
  v-model:open="open"
184
- title="Sandbox — prompt & model testing"
185
- description="Try prompt versions and models against graded fixtures, scored by a judge model."
212
+ :title="t('sandbox.title')"
213
+ :description="t('sandbox.description')"
186
214
  :ui="{ content: 'max-w-5xl' }"
187
215
  >
188
216
  <template #body>
@@ -194,21 +222,21 @@ async function archive(prompt: SandboxPromptVersion) {
194
222
  v-else-if="!store.available"
195
223
  class="rounded-lg border border-slate-700 bg-slate-900/50 p-6 text-sm text-slate-300"
196
224
  >
197
- <p class="font-medium text-slate-200">The Sandbox isn't enabled for this deployment.</p>
198
- <p class="mt-1 text-slate-400">
199
- It needs its own database (a dedicated <code>SANDBOX_DB</code> on Cloudflare, or the
200
- <code>sandbox</code> Postgres schema on Node). Provision it and reload.
201
- </p>
225
+ <p class="font-medium text-slate-200">{{ t('sandbox.unavailable.title') }}</p>
226
+ <i18n-t keypath="sandbox.unavailable.body" tag="p" class="mt-1 text-slate-400">
227
+ <template #db><code>SANDBOX_DB</code></template>
228
+ <template #schema><code>sandbox</code></template>
229
+ </i18n-t>
202
230
  </div>
203
231
 
204
232
  <div
205
233
  v-else-if="store.error"
206
234
  class="rounded-lg border border-rose-800 bg-rose-950/40 p-6 text-sm text-rose-200"
207
235
  >
208
- <p class="font-medium text-rose-100">The Sandbox failed to load.</p>
236
+ <p class="font-medium text-rose-100">{{ t('sandbox.error.title') }}</p>
209
237
  <p class="mt-1 text-rose-300">{{ store.error }}</p>
210
238
  <UButton class="mt-3" size="xs" color="neutral" variant="subtle" @click="store.load()">
211
- Retry
239
+ {{ t('common.retry') }}
212
240
  </UButton>
213
241
  </div>
214
242
 
@@ -216,9 +244,17 @@ async function archive(prompt: SandboxPromptVersion) {
216
244
  <UTabs
217
245
  v-model="tab"
218
246
  :items="[
219
- { label: 'Experiments', value: 'experiments', icon: 'i-lucide-flask-conical' },
220
- { label: 'Prompts', value: 'prompts', icon: 'i-lucide-file-text' },
221
- { label: 'Fixtures', value: 'fixtures', icon: 'i-lucide-clipboard-list' },
247
+ {
248
+ label: t('sandbox.tab.experiments'),
249
+ value: 'experiments',
250
+ icon: 'i-lucide-flask-conical',
251
+ },
252
+ { label: t('sandbox.tab.prompts'), value: 'prompts', icon: 'i-lucide-file-text' },
253
+ {
254
+ label: t('sandbox.tab.fixtures'),
255
+ value: 'fixtures',
256
+ icon: 'i-lucide-clipboard-list',
257
+ },
222
258
  ]"
223
259
  />
224
260
 
@@ -227,10 +263,10 @@ async function archive(prompt: SandboxPromptVersion) {
227
263
  <!-- builder -->
228
264
  <div class="space-y-3 rounded-lg border border-slate-700 bg-slate-900/40 p-3">
229
265
  <p class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
230
- New experiment
266
+ {{ t('sandbox.builder.title') }}
231
267
  </p>
232
268
 
233
- <UFormField label="Agent">
269
+ <UFormField :label="t('sandbox.builder.agent')">
234
270
  <USelect
235
271
  v-model="agentKind"
236
272
  :items="store.agentKinds.map((k) => ({ label: k.label, value: k.agentKind }))"
@@ -241,7 +277,7 @@ async function archive(prompt: SandboxPromptVersion) {
241
277
 
242
278
  <div>
243
279
  <span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
244
- Prompt versions
280
+ {{ t('sandbox.builder.promptVersions') }}
245
281
  </span>
246
282
  <div class="max-h-28 space-y-1 overflow-auto pr-1">
247
283
  <label
@@ -261,7 +297,11 @@ async function archive(prompt: SandboxPromptVersion) {
261
297
  variant="soft"
262
298
  size="xs"
263
299
  >
264
- {{ p.origin === 'baseline' ? 'baseline' : `v${p.version}` }}
300
+ {{
301
+ p.origin === 'baseline'
302
+ ? t('sandbox.baseline')
303
+ : t('sandbox.versionLabel', { version: p.version })
304
+ }}
265
305
  </UBadge>
266
306
  </label>
267
307
  </div>
@@ -269,7 +309,7 @@ async function archive(prompt: SandboxPromptVersion) {
269
309
 
270
310
  <div>
271
311
  <span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
272
- Models
312
+ {{ t('sandbox.builder.models') }}
273
313
  </span>
274
314
  <div class="max-h-28 space-y-1 overflow-auto pr-1">
275
315
  <label
@@ -286,14 +326,14 @@ async function archive(prompt: SandboxPromptVersion) {
286
326
  <span class="truncate">{{ m.label }}</span>
287
327
  </label>
288
328
  <p v-if="!store.selectableModels.length" class="text-xs text-slate-500">
289
- No selectable models — configure a provider key or enable Cloudflare AI.
329
+ {{ t('sandbox.builder.noModels') }}
290
330
  </p>
291
331
  </div>
292
332
  </div>
293
333
 
294
334
  <div>
295
335
  <span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
296
- Fixtures
336
+ {{ t('sandbox.builder.fixtures') }}
297
337
  </span>
298
338
  <div class="max-h-28 space-y-1 overflow-auto pr-1">
299
339
  <label
@@ -310,24 +350,30 @@ async function archive(prompt: SandboxPromptVersion) {
310
350
  <span class="truncate">{{ f.name }}</span>
311
351
  </label>
312
352
  <p v-if="!kindFixtures.length" class="text-xs text-slate-500">
313
- No fixtures for this agent.
353
+ {{ t('sandbox.builder.noFixtures') }}
314
354
  </p>
315
355
  </div>
316
356
  </div>
317
357
 
318
- <UFormField label="Judge model" hint="grades every cell">
358
+ <UFormField
359
+ :label="t('sandbox.builder.judgeModel')"
360
+ :hint="t('sandbox.builder.judgeModelHint')"
361
+ >
319
362
  <USelect v-model="selectedJudgeModel" :items="judgeModelItems" />
320
363
  </UFormField>
321
364
 
322
- <UFormField label="Name (optional)">
323
- <UInput v-model="name" :placeholder="`${agentKind} — sandbox run`" />
365
+ <UFormField :label="t('sandbox.builder.nameLabel')">
366
+ <UInput
367
+ v-model="name"
368
+ :placeholder="t('sandbox.defaultRunName', { kind: agentKind })"
369
+ />
324
370
  </UFormField>
325
371
 
326
372
  <div class="flex items-center justify-between">
327
373
  <span class="text-xs text-slate-500">
328
- {{ cellCount }} cell{{ cellCount === 1 ? '' : 's' }}
374
+ {{ t('sandbox.builder.cellCount', { count: cellCount }, cellCount) }}
329
375
  <span v-if="cellCount > store.maxCells" class="text-rose-400">
330
- (max {{ store.maxCells }})
376
+ {{ t('sandbox.builder.maxCells', { max: store.maxCells }) }}
331
377
  </span>
332
378
  </span>
333
379
  <UButton
@@ -338,7 +384,7 @@ async function archive(prompt: SandboxPromptVersion) {
338
384
  :disabled="!canRun"
339
385
  @click="createAndRun()"
340
386
  >
341
- Run
387
+ {{ t('sandbox.builder.run') }}
342
388
  </UButton>
343
389
  </div>
344
390
  </div>
@@ -350,17 +396,19 @@ async function archive(prompt: SandboxPromptVersion) {
350
396
  <p class="text-sm font-medium text-slate-200">
351
397
  {{ store.detail.experiment.name }}
352
398
  </p>
353
- <UBadge variant="soft" size="xs">{{ store.detail.experiment.status }}</UBadge>
399
+ <UBadge variant="soft" size="xs">{{
400
+ EXPERIMENT_STATUS_LABEL[store.detail.experiment.status]
401
+ }}</UBadge>
354
402
  </div>
355
403
  <div class="overflow-auto">
356
404
  <table class="w-full text-left text-xs">
357
405
  <thead class="text-slate-500">
358
406
  <tr>
359
- <th class="py-1 pr-2 font-medium">Prompt</th>
360
- <th class="py-1 pr-2 font-medium">Model</th>
361
- <th class="py-1 pr-2 font-medium">Fixture</th>
362
- <th class="py-1 pr-2 font-medium">Score</th>
363
- <th class="py-1 font-medium">Objective</th>
407
+ <th class="py-1 pr-2 font-medium">{{ t('sandbox.results.col.prompt') }}</th>
408
+ <th class="py-1 pr-2 font-medium">{{ t('sandbox.results.col.model') }}</th>
409
+ <th class="py-1 pr-2 font-medium">{{ t('sandbox.results.col.fixture') }}</th>
410
+ <th class="py-1 pr-2 font-medium">{{ t('sandbox.results.col.score') }}</th>
411
+ <th class="py-1 font-medium">{{ t('sandbox.results.col.objective') }}</th>
364
412
  </tr>
365
413
  </thead>
366
414
  <tbody>
@@ -383,9 +431,9 @@ async function archive(prompt: SandboxPromptVersion) {
383
431
  >
384
432
  {{ grade.weightedTotal.toFixed(2) }}
385
433
  </span>
386
- <span v-else-if="run.status === 'failed'" class="text-rose-400"
387
- >failed</span
388
- >
434
+ <span v-else-if="run.status === 'failed'" class="text-rose-400">{{
435
+ t('sandbox.results.failed')
436
+ }}</span>
389
437
  <span v-else class="text-slate-600">—</span>
390
438
  </td>
391
439
  <td class="py-1">
@@ -429,7 +477,9 @@ async function archive(prompt: SandboxPromptVersion) {
429
477
  </div>
430
478
  </div>
431
479
 
432
- <p class="text-[11px] uppercase tracking-wide text-slate-500">Past experiments</p>
480
+ <p class="text-[11px] uppercase tracking-wide text-slate-500">
481
+ {{ t('sandbox.results.past') }}
482
+ </p>
433
483
  <div class="max-h-56 space-y-1 overflow-auto">
434
484
  <button
435
485
  v-for="x in store.experiments"
@@ -438,10 +488,10 @@ async function archive(prompt: SandboxPromptVersion) {
438
488
  @click="store.openExperiment(x.id)"
439
489
  >
440
490
  <span class="truncate text-slate-300">{{ x.name }}</span>
441
- <UBadge variant="soft" size="xs">{{ x.status }}</UBadge>
491
+ <UBadge variant="soft" size="xs">{{ EXPERIMENT_STATUS_LABEL[x.status] }}</UBadge>
442
492
  </button>
443
493
  <p v-if="!store.experiments.length" class="text-xs text-slate-500">
444
- No experiments yet.
494
+ {{ t('sandbox.results.empty') }}
445
495
  </p>
446
496
  </div>
447
497
  </div>
@@ -463,7 +513,11 @@ async function archive(prompt: SandboxPromptVersion) {
463
513
  variant="soft"
464
514
  size="xs"
465
515
  >
466
- {{ p.origin === 'baseline' ? 'baseline' : `v${p.version}` }}
516
+ {{
517
+ p.origin === 'baseline'
518
+ ? t('sandbox.baseline')
519
+ : t('sandbox.versionLabel', { version: p.version })
520
+ }}
467
521
  </UBadge>
468
522
  </div>
469
523
  <span class="text-[11px] text-slate-500">{{ p.agentKind }}</span>
@@ -474,7 +528,11 @@ async function archive(prompt: SandboxPromptVersion) {
474
528
  color="neutral"
475
529
  variant="ghost"
476
530
  size="xs"
477
- :title="p.origin === 'baseline' ? 'Fork into a candidate' : 'Edit / version'"
531
+ :title="
532
+ p.origin === 'baseline'
533
+ ? t('sandbox.prompts.forkTitle')
534
+ : t('sandbox.prompts.editTitle')
535
+ "
478
536
  @click="edit(p)"
479
537
  />
480
538
  <UButton
@@ -494,12 +552,16 @@ async function archive(prompt: SandboxPromptVersion) {
494
552
  class="space-y-2 rounded-lg border border-slate-700 bg-slate-900/40 p-3"
495
553
  >
496
554
  <p class="text-[11px] uppercase tracking-wide text-slate-500">
497
- {{ editing.origin === 'baseline' ? 'Fork' : 'New version of' }} · {{ editing.name }}
555
+ {{
556
+ editing.origin === 'baseline'
557
+ ? t('sandbox.prompts.forkOf', { name: editing.name })
558
+ : t('sandbox.prompts.newVersionOf', { name: editing.name })
559
+ }}
498
560
  </p>
499
561
  <UTextarea v-model="editText" :rows="16" class="w-full font-mono text-xs" autoresize />
500
562
  <div class="flex justify-end gap-2">
501
563
  <UButton color="neutral" variant="ghost" size="sm" @click="editing = null">
502
- Cancel
564
+ {{ t('common.cancel') }}
503
565
  </UButton>
504
566
  <UButton
505
567
  color="primary"
@@ -509,13 +571,12 @@ async function archive(prompt: SandboxPromptVersion) {
509
571
  :disabled="!editText.trim()"
510
572
  @click="saveVersion()"
511
573
  >
512
- Save new version
574
+ {{ t('sandbox.prompts.saveVersion') }}
513
575
  </UButton>
514
576
  </div>
515
577
  </div>
516
578
  <p v-else class="self-start text-xs text-slate-500">
517
- Pick a prompt to fork a shipped baseline or version a candidate. Each save appends an
518
- immutable version you can put under test.
579
+ {{ t('sandbox.prompts.hint') }}
519
580
  </p>
520
581
  </div>
521
582
 
@@ -529,23 +590,29 @@ async function archive(prompt: SandboxPromptVersion) {
529
590
  <div class="flex items-center justify-between">
530
591
  <span class="text-slate-200">{{ f.name }}</span>
531
592
  <div class="flex items-center gap-1.5">
532
- <UBadge variant="soft" size="xs">{{ f.kind }}</UBadge>
593
+ <UBadge variant="soft" size="xs">{{ FIXTURE_KIND_LABEL[f.kind] }}</UBadge>
533
594
  <UBadge
534
595
  :color="f.origin === 'builtin' ? 'neutral' : 'primary'"
535
596
  variant="soft"
536
597
  size="xs"
537
598
  >
538
- {{ f.origin }}
599
+ {{ FIXTURE_ORIGIN_LABEL[f.origin] }}
539
600
  </UBadge>
540
601
  </div>
541
602
  </div>
542
603
  <p v-if="f.objective?.kind === 'findings'" class="mt-0.5 text-[11px] text-slate-500">
543
- {{ f.objective.expectations.length }} graded expectation{{
544
- f.objective.expectations.length === 1 ? '' : 's'
604
+ {{
605
+ t(
606
+ 'sandbox.fixtures.expectations',
607
+ { count: f.objective.expectations.length },
608
+ f.objective.expectations.length,
609
+ )
545
610
  }}
546
611
  </p>
547
612
  </div>
548
- <p v-if="!store.fixtures.length" class="text-xs text-slate-500">No fixtures.</p>
613
+ <p v-if="!store.fixtures.length" class="text-xs text-slate-500">
614
+ {{ t('sandbox.fixtures.empty') }}
615
+ </p>
549
616
  </div>
550
617
  </div>
551
618
  </template>
@@ -14,11 +14,7 @@ import InspectorPanel from '~/components/panels/InspectorPanel.vue'
14
14
  import DecisionModal from '~/components/panels/DecisionModal.vue'
15
15
  import AgentStepDetail from '~/components/panels/AgentStepDetail.vue'
16
16
  import StepResultViewHost from '~/components/panels/StepResultViewHost.vue'
17
- import BlockFocusView from '~/components/focus/BlockFocusView.vue'
18
- import TaskSourceConnectModal from '~/components/tasks/TaskSourceConnectModal.vue'
19
- import TaskImportModal from '~/components/tasks/TaskImportModal.vue'
20
17
  import AddTaskModal from '~/components/board/AddTaskModal.vue'
21
- import RecurringPipelineModal from '~/components/board/RecurringPipelineModal.vue'
22
18
  import GitHubOnboarding from '~/components/github/GitHubOnboarding.vue'
23
19
  import CommandBar from '~/components/layout/CommandBar.vue'
24
20
  import PersonalCredentialModal from '~/components/providers/PersonalCredentialModal.vue'
@@ -30,6 +26,17 @@ const ObservabilityPanel = defineAsyncComponent(
30
26
  () => import('~/components/panels/ObservabilityPanel.vue'),
31
27
  )
32
28
  const KaizenPanel = defineAsyncComponent(() => import('~/components/kaizen/KaizenPanel.vue'))
29
+ // Occasional, externally store-gated surfaces — deferred to their own chunks like the
30
+ // sibling document modals above. Each mounts only while its ui open-flag is set, so it
31
+ // loads on first open instead of bloating the initial bundle.
32
+ const BlockFocusView = defineAsyncComponent(() => import('~/components/focus/BlockFocusView.vue'))
33
+ const TaskSourceConnectModal = defineAsyncComponent(
34
+ () => import('~/components/tasks/TaskSourceConnectModal.vue'),
35
+ )
36
+ const TaskImportModal = defineAsyncComponent(() => import('~/components/tasks/TaskImportModal.vue'))
37
+ const RecurringPipelineModal = defineAsyncComponent(
38
+ () => import('~/components/board/RecurringPipelineModal.vue'),
39
+ )
33
40
  const DocumentSourceConnectModal = defineAsyncComponent(
34
41
  () => import('~/components/documents/DocumentSourceConnectModal.vue'),
35
42
  )
@@ -248,7 +255,12 @@ watch(
248
255
  <BoardToolbar />
249
256
  <SpendWarningBanner />
250
257
  <InspectorPanel />
251
- <BlockFocusView />
258
+ <!-- Code-split focus view. The fade lives here (not inside the component) so the
259
+ leave animation still plays when `focusBlockId` clears and the v-if unmounts
260
+ the chunk — an inner Transition would be torn down before it could run. -->
261
+ <Transition name="focus-fade">
262
+ <BlockFocusView v-if="ui.focusBlockId" />
263
+ </Transition>
252
264
  </main>
253
265
 
254
266
  <!-- Always-mounted, fast-path surfaces. -->
@@ -256,15 +268,15 @@ watch(
256
268
  <DecisionModal />
257
269
  <AgentStepDetail />
258
270
  <StepResultViewHost />
259
- <TaskSourceConnectModal />
260
- <TaskImportModal />
261
271
  <AddTaskModal />
262
- <RecurringPipelineModal />
263
272
  <CommandBar />
264
273
  <PersonalCredentialModal />
265
274
 
266
275
  <!-- Lazy panels: mounted only while their ui open-flag is set, so each loads on
267
276
  first open (its own chunk) rather than bloating the initial bundle. -->
277
+ <TaskSourceConnectModal v-if="ui.taskConnect" />
278
+ <TaskImportModal v-if="ui.taskImport" />
279
+ <RecurringPipelineModal v-if="ui.addRecurringFrameId" />
268
280
  <ObservabilityPanel v-if="ui.observabilityInstanceId" />
269
281
  <KaizenPanel v-if="ui.kaizenScreenOpen" />
270
282
  <DocumentSourceConnectModal v-if="ui.documentConnect" />
@@ -113,11 +113,16 @@ export const useWorkspaceStore = defineStore(
113
113
  ready.value = false
114
114
  error.value = null
115
115
  try {
116
- // Accounts are an auth concept — empty in dev, which leaves boards unscoped.
117
- await useAccountsStore()
118
- .load()
119
- .catch(() => {})
120
- workspaces.value = await api.listWorkspaces()
116
+ // Accounts (an auth concept — empty in dev, which leaves boards unscoped) and the
117
+ // workspace list are independent, so fetch them concurrently. resolveActiveBoard
118
+ // needs both, so it still runs after.
119
+ const [, workspaceList] = await Promise.all([
120
+ useAccountsStore()
121
+ .load()
122
+ .catch(() => {}),
123
+ api.listWorkspaces(),
124
+ ])
125
+ workspaces.value = workspaceList
121
126
  await resolveActiveBoard()
122
127
  ready.value = true
123
128
  } catch (e) {