@cat-factory/app 0.46.8 → 0.46.10

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.
@@ -26,6 +26,7 @@ const modelPresets = useModelPresetsStore()
26
26
  const pipelines = usePipelinesStore()
27
27
  const agentConfig = useAgentConfigStore()
28
28
  const toast = useToast()
29
+ const { t } = useI18n()
29
30
 
30
31
  const { linkPending } = useContextLinking()
31
32
 
@@ -52,13 +53,13 @@ const technical = ref(false)
52
53
  // delegates to <RecurringPipelineModal> instead of creating a one-off task here.
53
54
  type TaskTypeChoice = CreateTaskType | 'recurring'
54
55
  const taskType = ref<TaskTypeChoice>('feature')
55
- const TASK_TYPES: { value: TaskTypeChoice; label: string; icon: string }[] = [
56
- { value: 'feature', label: 'Feature', icon: 'i-lucide-sparkles' },
57
- { value: 'bug', label: 'Bug', icon: 'i-lucide-bug' },
58
- { value: 'document', label: 'Document', icon: 'i-lucide-file-text' },
59
- { value: 'spike', label: 'Spike', icon: 'i-lucide-flask-conical' },
60
- { value: 'recurring', label: 'Recurring', icon: 'i-lucide-repeat' },
61
- ]
56
+ const TASK_TYPES = computed<{ value: TaskTypeChoice; label: string; icon: string }[]>(() => [
57
+ { value: 'feature', label: t('board.addTask.types.feature'), icon: 'i-lucide-sparkles' },
58
+ { value: 'bug', label: t('board.addTask.types.bug'), icon: 'i-lucide-bug' },
59
+ { value: 'document', label: t('board.addTask.types.document'), icon: 'i-lucide-file-text' },
60
+ { value: 'spike', label: t('board.addTask.types.spike'), icon: 'i-lucide-flask-conical' },
61
+ { value: 'recurring', label: t('board.addTask.types.recurring'), icon: 'i-lucide-repeat' },
62
+ ])
62
63
  const isRecurring = computed(() => taskType.value === 'recurring')
63
64
 
64
65
  // Per-type fields (only the ones relevant to the chosen type are shown / sent).
@@ -114,12 +115,18 @@ const mergePresetId = ref('')
114
115
  const modelPresetId = ref('')
115
116
  const pipelineId = ref('')
116
117
 
118
+ const defaultPresetLabel = computed(() =>
119
+ mergePresets.defaultPreset
120
+ ? t('board.addTask.defaultPreset', {
121
+ name: mergePresets.defaultPreset.name,
122
+ thresholds: mergePresetThresholds(mergePresets.defaultPreset),
123
+ })
124
+ : t('board.addTask.workspaceDefault'),
125
+ )
117
126
  const presetMenu = computed(() => [
118
127
  [
119
128
  {
120
- label: mergePresets.defaultPreset
121
- ? `Default (${mergePresets.defaultPreset.name}) — ${mergePresetThresholds(mergePresets.defaultPreset)}`
122
- : 'Workspace default',
129
+ label: defaultPresetLabel.value,
123
130
  icon: 'i-lucide-rotate-ccw',
124
131
  onSelect: () => (mergePresetId.value = ''),
125
132
  },
@@ -131,22 +138,21 @@ const presetMenu = computed(() => [
131
138
  ],
132
139
  ])
133
140
  const selectedPresetLabel = computed(() => {
134
- if (!mergePresetId.value) {
135
- return mergePresets.defaultPreset
136
- ? `Default (${mergePresets.defaultPreset.name}) — ${mergePresetThresholds(mergePresets.defaultPreset)}`
137
- : 'Workspace default'
138
- }
141
+ if (!mergePresetId.value) return defaultPresetLabel.value
139
142
  const picked = mergePresets.presets.find((p) => p.id === mergePresetId.value)
140
- return picked ? mergePresetOptionLabel(picked) : 'Workspace default'
143
+ return picked ? mergePresetOptionLabel(picked) : t('board.addTask.workspaceDefault')
141
144
  })
142
145
 
143
146
  // Model preset: which model each agent runs on. Empty = workspace default preset.
147
+ const defaultModelPresetLabel = computed(() =>
148
+ modelPresets.defaultPreset
149
+ ? t('board.addTask.defaultModelPreset', { name: modelPresets.defaultPreset.name })
150
+ : t('board.addTask.workspaceDefault'),
151
+ )
144
152
  const modelPresetMenu = computed(() => [
145
153
  [
146
154
  {
147
- label: modelPresets.defaultPreset
148
- ? `Default (${modelPresets.defaultPreset.name})`
149
- : 'Workspace default',
155
+ label: defaultModelPresetLabel.value,
150
156
  icon: 'i-lucide-rotate-ccw',
151
157
  onSelect: () => (modelPresetId.value = ''),
152
158
  },
@@ -158,18 +164,17 @@ const modelPresetMenu = computed(() => [
158
164
  ],
159
165
  ])
160
166
  const selectedModelPresetLabel = computed(() => {
161
- if (!modelPresetId.value) {
162
- return modelPresets.defaultPreset
163
- ? `Default (${modelPresets.defaultPreset.name})`
164
- : 'Workspace default'
165
- }
166
- return modelPresets.presets.find((p) => p.id === modelPresetId.value)?.name ?? 'Workspace default'
167
+ if (!modelPresetId.value) return defaultModelPresetLabel.value
168
+ return (
169
+ modelPresets.presets.find((p) => p.id === modelPresetId.value)?.name ??
170
+ t('board.addTask.workspaceDefault')
171
+ )
167
172
  })
168
173
 
169
174
  const pipelineMenu = computed(() => [
170
175
  [
171
176
  {
172
- label: 'Choose at run time',
177
+ label: t('board.addTask.chooseAtRunTime'),
173
178
  icon: 'i-lucide-rotate-ccw',
174
179
  onSelect: () => (pipelineId.value = ''),
175
180
  },
@@ -181,7 +186,7 @@ const pipelineMenu = computed(() => [
181
186
  ],
182
187
  ])
183
188
  const selectedPipelineLabel = computed(
184
- () => pipelines.getPipeline(pipelineId.value)?.name ?? 'Choose at run time',
189
+ () => pipelines.getPipeline(pipelineId.value)?.name ?? t('board.addTask.chooseAtRunTime'),
185
190
  )
186
191
 
187
192
  // Task-level agent config contributed by the selected pipeline's agents (e.g. the
@@ -362,7 +367,7 @@ async function add() {
362
367
  const failed = await linkPending(block.id, pendingContext.value)
363
368
  if (failed > 0) {
364
369
  toast.add({
365
- title: `Task added, but ${failed} attachment${failed === 1 ? '' : 's'} could not be linked`,
370
+ title: t('board.addTask.linkFailed', { count: failed }, failed),
366
371
  icon: 'i-lucide-triangle-alert',
367
372
  color: 'warning',
368
373
  })
@@ -371,7 +376,7 @@ async function add() {
371
376
  ui.closeAddTask()
372
377
  } catch (e) {
373
378
  toast.add({
374
- title: 'Could not add task',
379
+ title: t('board.addTask.addFailedTitle'),
375
380
  description: e instanceof Error ? e.message : String(e),
376
381
  icon: 'i-lucide-triangle-alert',
377
382
  color: 'error',
@@ -383,25 +388,29 @@ async function add() {
383
388
  </script>
384
389
 
385
390
  <template>
386
- <UModal v-model:open="open" title="Add a task">
391
+ <UModal v-model:open="open" :title="t('board.addTask.title')">
387
392
  <template #body>
388
393
  <div class="space-y-4" data-testid="add-task-modal">
389
394
  <p v-if="container" class="text-xs text-slate-400">
390
- New task in <span class="font-medium text-slate-200">{{ container.title }}</span>
395
+ <i18n-t keypath="board.addTask.newTaskIn" tag="span" scope="global">
396
+ <template #container>
397
+ <span class="font-medium text-slate-200">{{ container.title }}</span>
398
+ </template>
399
+ </i18n-t>
391
400
  </p>
392
401
 
393
- <UFormField label="Type">
402
+ <UFormField :label="t('board.addTask.typeLabel')">
394
403
  <div class="flex flex-wrap gap-1">
395
404
  <UButton
396
- v-for="t in TASK_TYPES"
397
- :key="t.value"
398
- :color="taskType === t.value ? 'primary' : 'neutral'"
399
- :variant="taskType === t.value ? 'soft' : 'ghost'"
400
- :icon="t.icon"
405
+ v-for="ty in TASK_TYPES"
406
+ :key="ty.value"
407
+ :color="taskType === ty.value ? 'primary' : 'neutral'"
408
+ :variant="taskType === ty.value ? 'soft' : 'ghost'"
409
+ :icon="ty.icon"
401
410
  size="xs"
402
- @click="taskType = t.value"
411
+ @click="taskType = ty.value"
403
412
  >
404
- {{ t.label }}
413
+ {{ ty.label }}
405
414
  </UButton>
406
415
  </div>
407
416
  </UFormField>
@@ -412,20 +421,19 @@ async function add() {
412
421
  class="rounded-lg border border-slate-800 p-3 text-[11px] text-slate-400"
413
422
  >
414
423
  <template v-if="recurringFrameId">
415
- A recurring task runs a pipeline on a cadence. Continue to set the schedule + prompt.
424
+ {{ t('board.addTask.recurringWithFrame') }}
416
425
  </template>
417
426
  <template v-else>
418
- A recurring task must live on a service. Add it from a service frame (or a module inside
419
- one).
427
+ {{ t('board.addTask.recurringNoFrame') }}
420
428
  </template>
421
429
  </div>
422
430
 
423
431
  <template v-if="!isRecurring">
424
- <UFormField label="Title" required>
432
+ <UFormField :label="t('board.addTask.titleField')" required>
425
433
  <UInput
426
434
  v-model="title"
427
435
  data-testid="add-task-title"
428
- placeholder="What needs to be done?"
436
+ :placeholder="t('board.addTask.titlePlaceholder')"
429
437
  autofocus
430
438
  class="w-full"
431
439
  @keydown.enter="add"
@@ -438,7 +446,7 @@ async function add() {
438
446
  <UFormField
439
447
  v-for="issue in linkedIssueBodies"
440
448
  :key="issue.key"
441
- :label="`${issue.title} (from issue, included)`"
449
+ :label="t('board.addTask.issueIncluded', { title: issue.title })"
442
450
  >
443
451
  <UTextarea
444
452
  :model-value="issue.body"
@@ -450,18 +458,24 @@ async function add() {
450
458
  />
451
459
  </UFormField>
452
460
  <p v-if="resolvingIssueBodies" class="text-[11px] text-slate-500">
453
- Loading the linked issue's description…
461
+ {{ t('board.addTask.loadingIssue') }}
454
462
  </p>
455
463
 
456
- <UFormField :label="hasLinkedIssueBody ? 'Additional notes' : 'Description'">
464
+ <UFormField
465
+ :label="
466
+ hasLinkedIssueBody
467
+ ? t('board.addTask.additionalNotes')
468
+ : t('board.addTask.description')
469
+ "
470
+ >
457
471
  <UTextarea
458
472
  v-model="description"
459
473
  :rows="4"
460
474
  autoresize
461
475
  :placeholder="
462
476
  hasLinkedIssueBody
463
- ? 'Add anything else the agent should know — appended to the issue description above…'
464
- : 'Describe the work — context, acceptance criteria, anything the agent should know…'
477
+ ? t('board.addTask.notesPlaceholder')
478
+ : t('board.addTask.descriptionPlaceholder')
465
479
  "
466
480
  class="w-full"
467
481
  />
@@ -469,20 +483,18 @@ async function add() {
469
483
 
470
484
  <UCheckbox v-model="technical" name="technical">
471
485
  <template #label>
472
- <span class="text-sm text-slate-200">Technical task</span>
486
+ <span class="text-sm text-slate-200">{{ t('board.addTask.technical') }}</span>
473
487
  </template>
474
488
  <template #description>
475
489
  <span class="text-[11px] text-slate-500">
476
- A refactor / non-functional / internal change. The implementer treats the task
477
- definition as primary and the spec as a regression reference; leave off to let the
478
- spec phase decide.
490
+ {{ t('board.addTask.technicalHint') }}
479
491
  </span>
480
492
  </template>
481
493
  </UCheckbox>
482
494
 
483
495
  <!-- Per-type fields. -->
484
496
  <div v-if="taskType === 'bug'" class="grid grid-cols-2 gap-3">
485
- <UFormField label="Severity">
497
+ <UFormField :label="t('board.addTask.severity')">
486
498
  <div class="flex flex-wrap gap-1">
487
499
  <UButton
488
500
  v-for="s in SEVERITIES"
@@ -497,29 +509,29 @@ async function add() {
497
509
  </UButton>
498
510
  </div>
499
511
  </UFormField>
500
- <UFormField label="Steps to reproduce" class="col-span-2">
512
+ <UFormField :label="t('board.addTask.stepsToReproduce')" class="col-span-2">
501
513
  <UTextarea
502
514
  v-model="stepsToReproduce"
503
515
  :rows="2"
504
516
  autoresize
505
- placeholder="Observed vs expected, and how to reproduce…"
517
+ :placeholder="t('board.addTask.stepsToReproducePlaceholder')"
506
518
  class="w-full"
507
519
  />
508
520
  </UFormField>
509
521
  </div>
510
522
 
511
- <UFormField v-else-if="taskType === 'spike'" label="Time-box (hours)">
523
+ <UFormField v-else-if="taskType === 'spike'" :label="t('board.addTask.timebox')">
512
524
  <UInput
513
525
  v-model.number="timeboxHours"
514
526
  type="number"
515
527
  min="0"
516
- placeholder="e.g. 8"
528
+ :placeholder="t('board.addTask.timeboxPlaceholder')"
517
529
  class="w-full"
518
530
  />
519
531
  </UFormField>
520
532
 
521
533
  <div v-else-if="taskType === 'document'" class="space-y-3">
522
- <UFormField label="Document kind">
534
+ <UFormField :label="t('board.addTask.documentKind')">
523
535
  <div class="flex flex-wrap gap-1">
524
536
  <UButton
525
537
  v-for="k in DOC_KINDS"
@@ -535,33 +547,39 @@ async function add() {
535
547
  </div>
536
548
  </UFormField>
537
549
  <div class="grid grid-cols-2 gap-3">
538
- <UFormField label="Audience" hint="optional">
550
+ <UFormField :label="t('board.addTask.audience')" :hint="t('board.addTask.optional')">
539
551
  <UInput
540
552
  v-model="docAudience"
541
- placeholder="e.g. platform engineers"
553
+ :placeholder="t('board.addTask.audiencePlaceholder')"
542
554
  class="w-full"
543
555
  />
544
556
  </UFormField>
545
- <UFormField label="Target path" hint="optional">
557
+ <UFormField
558
+ :label="t('board.addTask.targetPath')"
559
+ :hint="t('board.addTask.optional')"
560
+ >
546
561
  <UInput
547
562
  v-model="docTargetPath"
548
- placeholder="e.g. docs/rfcs/0001-foo.md"
563
+ :placeholder="t('board.addTask.targetPathPlaceholder')"
549
564
  class="w-full"
550
565
  />
551
566
  </UFormField>
552
567
  </div>
553
- <UFormField label="Outline hints" hint="optional">
568
+ <UFormField
569
+ :label="t('board.addTask.outlineHints')"
570
+ :hint="t('board.addTask.optional')"
571
+ >
554
572
  <UTextarea
555
573
  v-model="docOutlineHints"
556
574
  :rows="2"
557
- placeholder="Sections or points the document should cover"
575
+ :placeholder="t('board.addTask.outlineHintsPlaceholder')"
558
576
  class="w-full"
559
577
  />
560
578
  </UFormField>
561
579
  </div>
562
580
 
563
581
  <div class="grid grid-cols-2 gap-3">
564
- <UFormField label="Pipeline">
582
+ <UFormField :label="t('board.addTask.pipeline')">
565
583
  <UDropdownMenu :items="pipelineMenu" class="w-full">
566
584
  <UButton
567
585
  color="neutral"
@@ -576,7 +594,7 @@ async function add() {
576
594
  </UDropdownMenu>
577
595
  </UFormField>
578
596
 
579
- <UFormField label="Merge policy">
597
+ <UFormField :label="t('board.addTask.mergePolicy')">
580
598
  <UDropdownMenu :items="presetMenu" class="w-full">
581
599
  <UButton
582
600
  color="neutral"
@@ -591,7 +609,7 @@ async function add() {
591
609
  </UDropdownMenu>
592
610
  </UFormField>
593
611
 
594
- <UFormField label="Model preset">
612
+ <UFormField :label="t('board.addTask.modelPreset')">
595
613
  <UDropdownMenu :items="modelPresetMenu" class="w-full">
596
614
  <UButton
597
615
  color="neutral"
@@ -609,7 +627,7 @@ async function add() {
609
627
 
610
628
  <div v-if="configDescriptors.length" class="space-y-3">
611
629
  <span class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
612
- Agent configuration
630
+ {{ t('board.addTask.agentConfiguration') }}
613
631
  </span>
614
632
  <div v-for="d in configDescriptors" :key="d.id" class="space-y-1">
615
633
  <div class="text-[11px] text-slate-400">{{ d.label }}</div>
@@ -633,7 +651,7 @@ async function add() {
633
651
  <div class="space-y-2">
634
652
  <div class="flex items-center justify-between">
635
653
  <span class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
636
- Context documents
654
+ {{ t('board.addTask.contextDocuments') }}
637
655
  </span>
638
656
  <UButton
639
657
  v-if="docsConnected"
@@ -643,7 +661,7 @@ async function add() {
643
661
  :icon="showDocPicker ? 'i-lucide-x' : 'i-lucide-plus'"
644
662
  @click="showDocPicker = !showDocPicker"
645
663
  >
646
- {{ showDocPicker ? 'Done' : 'Attach' }}
664
+ {{ showDocPicker ? t('board.addTask.done') : t('board.addTask.attach') }}
647
665
  </UButton>
648
666
  <UButton
649
667
  v-else
@@ -654,11 +672,11 @@ async function add() {
654
672
  disabled
655
673
  :title="
656
674
  documents.available
657
- ? 'Connect a document source first (Integrations)'
658
- : 'Enable the documents integration first'
675
+ ? t('board.addTask.attachDocDisabledConnect')
676
+ : t('board.addTask.attachDocDisabledEnable')
659
677
  "
660
678
  >
661
- Attach
679
+ {{ t('board.addTask.attach') }}
662
680
  </UButton>
663
681
  </div>
664
682
  <ContextDocumentPicker
@@ -684,7 +702,7 @@ async function add() {
684
702
  size="xs"
685
703
  class="ml-1 shrink-0"
686
704
  >
687
- imports on add
705
+ {{ t('board.addTask.importsOnAdd') }}
688
706
  </UBadge>
689
707
  <button
690
708
  type="button"
@@ -696,7 +714,7 @@ async function add() {
696
714
  </div>
697
715
  </div>
698
716
  <p v-else class="text-[11px] text-slate-500">
699
- Attach a requirement, RFC or PRD so agents see it while implementing this task.
717
+ {{ t('board.addTask.noDocsHint') }}
700
718
  </p>
701
719
  </div>
702
720
 
@@ -704,7 +722,7 @@ async function add() {
704
722
  <div class="space-y-2">
705
723
  <div class="flex items-center justify-between">
706
724
  <span class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
707
- Context issues
725
+ {{ t('board.addTask.contextIssues') }}
708
726
  </span>
709
727
  <UButton
710
728
  v-if="issuesConnected"
@@ -714,7 +732,7 @@ async function add() {
714
732
  :icon="showIssuePicker ? 'i-lucide-x' : 'i-lucide-plus'"
715
733
  @click="showIssuePicker = !showIssuePicker"
716
734
  >
717
- {{ showIssuePicker ? 'Done' : 'Attach' }}
735
+ {{ showIssuePicker ? t('board.addTask.done') : t('board.addTask.attach') }}
718
736
  </UButton>
719
737
  <UButton
720
738
  v-else
@@ -725,11 +743,11 @@ async function add() {
725
743
  disabled
726
744
  :title="
727
745
  tasks.available
728
- ? 'Connect an issue tracker first (Integrations)'
729
- : 'Enable the issue-tracker integration first'
746
+ ? t('board.addTask.attachIssueDisabledConnect')
747
+ : t('board.addTask.attachIssueDisabledEnable')
730
748
  "
731
749
  >
732
- Attach
750
+ {{ t('board.addTask.attach') }}
733
751
  </UButton>
734
752
  </div>
735
753
  <ContextIssuePicker
@@ -756,7 +774,7 @@ async function add() {
756
774
  size="xs"
757
775
  class="ml-1 shrink-0"
758
776
  >
759
- imports on add
777
+ {{ t('board.addTask.importsOnAdd') }}
760
778
  </UBadge>
761
779
  <button
762
780
  type="button"
@@ -768,14 +786,12 @@ async function add() {
768
786
  </div>
769
787
  </div>
770
788
  <p v-else class="text-[11px] text-slate-500">
771
- Attach a tracker issue so agents see its description and comments while implementing
772
- this task.
789
+ {{ t('board.addTask.noIssuesHint') }}
773
790
  </p>
774
791
  </div>
775
792
 
776
793
  <p class="text-[11px] text-slate-500">
777
- The task is added in a planned state. It won't run until you start a pipeline on it —
778
- you can keep editing it until then.
794
+ {{ t('board.addTask.plannedHint') }}
779
795
  </p>
780
796
  </template>
781
797
  </div>
@@ -783,7 +799,9 @@ async function add() {
783
799
 
784
800
  <template #footer>
785
801
  <div class="flex w-full justify-end gap-2">
786
- <UButton color="neutral" variant="ghost" @click="ui.closeAddTask()">Cancel</UButton>
802
+ <UButton color="neutral" variant="ghost" @click="ui.closeAddTask()">{{
803
+ t('common.cancel')
804
+ }}</UButton>
787
805
  <UButton
788
806
  color="primary"
789
807
  data-testid="add-task-submit"
@@ -792,7 +810,7 @@ async function add() {
792
810
  :disabled="!canAdd"
793
811
  @click="add"
794
812
  >
795
- {{ isRecurring ? 'Continue' : 'Add task' }}
813
+ {{ isRecurring ? t('board.addTask.continue') : t('board.addTask.submit') }}
796
814
  </UButton>
797
815
  </div>
798
816
  </template>
@@ -11,6 +11,7 @@ const props = withDefaults(
11
11
  { variant: 'expanded' },
12
12
  )
13
13
 
14
+ const { t } = useI18n()
14
15
  const agentRuns = useAgentRunsStore()
15
16
 
16
17
  const compact = computed(() => props.variant === 'compact')
@@ -19,11 +20,13 @@ const title = computed(() => {
19
20
  // A `dispatch` failure means the container/runner never accepted the job — say so
20
21
  // explicitly rather than the generic "Run failed", and show the verbatim provider
21
22
  // error in the collapsible detail below.
22
- if (failure.value?.kind === 'dispatch') return 'Container failed to start'
23
- return props.run.kind === 'bootstrap' ? 'Bootstrap failed' : 'Run failed'
23
+ if (failure.value?.kind === 'dispatch') return t('board.failure.containerFailedToStart')
24
+ return props.run.kind === 'bootstrap'
25
+ ? t('board.failure.bootstrapFailed')
26
+ : t('board.failure.runFailed')
24
27
  })
25
28
  const retryLabel = computed(() =>
26
- props.run.kind === 'bootstrap' ? 'Retry bootstrap' : 'Retry run',
29
+ props.run.kind === 'bootstrap' ? t('board.failure.retryBootstrap') : t('board.failure.retryRun'),
27
30
  )
28
31
 
29
32
  const retrying = ref(false)
@@ -73,7 +76,7 @@ async function retry() {
73
76
 
74
77
  <details v-if="!compact && failure?.detail && failure.detail !== failure.message" class="mt-1">
75
78
  <summary class="cursor-pointer text-[10px] text-rose-400/60 hover:text-rose-300">
76
- Show detail
79
+ {{ t('board.failure.showDetail') }}
77
80
  </summary>
78
81
  <pre
79
82
  class="mt-1 max-h-32 overflow-auto whitespace-pre-wrap rounded bg-rose-950/60 p-1.5 text-[10px] text-rose-200/80"
@@ -92,7 +95,7 @@ async function retry() {
92
95
  :name="retrying ? 'i-lucide-loader-circle' : 'i-lucide-rotate-ccw'"
93
96
  :class="[compact ? 'h-3 w-3' : 'h-3.5 w-3.5', { 'animate-spin': retrying }]"
94
97
  />
95
- {{ retrying ? 'Retrying…' : compact ? 'Retry' : retryLabel }}
98
+ {{ retrying ? t('board.failure.retrying') : compact ? t('common.retry') : retryLabel }}
96
99
  </button>
97
100
  </div>
98
101
  </template>
@@ -16,27 +16,30 @@ const props = withDefaults(
16
16
  variant?: 'solid' | 'soft' | 'ghost' | 'subtle' | 'outline'
17
17
  label?: string
18
18
  }>(),
19
- { size: 'xs', variant: 'soft', label: 'Stop' },
19
+ { size: 'xs', variant: 'soft' },
20
20
  )
21
21
 
22
+ const { t } = useI18n()
22
23
  const agentRuns = useAgentRunsStore()
23
24
  const toast = useToast()
24
25
  const stopping = ref(false)
25
26
 
27
+ const displayLabel = computed(() => props.label ?? t('board.stop.label'))
28
+
26
29
  async function stop() {
27
30
  if (stopping.value) return
28
31
  stopping.value = true
29
32
  try {
30
33
  const kind = await agentRuns.stop(props.runId)
31
34
  toast.add({
32
- title: kind === 'bootstrap' ? 'Bootstrap stopped' : 'Run stopped',
33
- description: 'The container was killed and the run was cancelled.',
35
+ title: kind === 'bootstrap' ? t('board.stop.bootstrapStopped') : t('board.stop.runStopped'),
36
+ description: t('board.stop.stoppedDescription'),
34
37
  icon: 'i-lucide-circle-stop',
35
38
  color: 'warning',
36
39
  })
37
40
  } catch (e) {
38
41
  toast.add({
39
- title: 'Stop failed',
42
+ title: t('board.stop.stopFailed'),
40
43
  description: e instanceof Error ? e.message : String(e),
41
44
  color: 'error',
42
45
  })
@@ -56,6 +59,6 @@ async function stop() {
56
59
  :loading="stopping"
57
60
  @click.stop="stop"
58
61
  >
59
- {{ label }}
62
+ {{ displayLabel }}
60
63
  </UButton>
61
64
  </template>
@@ -19,6 +19,7 @@ const execution = useExecutionStore()
19
19
  const ui = useUiStore()
20
20
  const github = useGitHubStore()
21
21
  const toast = useToast()
22
+ const { t } = useI18n()
22
23
 
23
24
  const { onNodeDragStop, onViewportChange, screenToFlowCoordinate } = useVueFlow(BOARD_FLOW_ID)
24
25
  const { draggingId } = useBlockDrag()
@@ -126,8 +127,8 @@ async function onDrop(event: DragEvent) {
126
127
  ui.select(block.id)
127
128
  } catch {
128
129
  toast.add({
129
- title: 'Could not add block',
130
- description: 'The backend rejected the request.',
130
+ title: t('board.canvas.addBlockFailedTitle'),
131
+ description: t('board.canvas.addBlockFailedBody'),
131
132
  color: 'error',
132
133
  })
133
134
  }
@@ -143,13 +144,16 @@ async function onDrop(event: DragEvent) {
143
144
  if (!target || !pipeline) return
144
145
  if (target.level !== 'task') {
145
146
  toast.add({
146
- title: 'Drop onto a task',
147
- description: 'Pipelines run against tasks, not services.',
147
+ title: t('board.canvas.dropOntoTaskTitle'),
148
+ description: t('board.canvas.dropOntoTaskBody'),
148
149
  })
149
150
  return
150
151
  }
151
152
  if (!board.isRunnable(target.id)) {
152
- toast.add({ title: 'Task is blocked', description: 'Its dependencies haven’t merged yet.' })
153
+ toast.add({
154
+ title: t('board.canvas.taskBlockedTitle'),
155
+ description: t('board.canvas.taskBlockedBody'),
156
+ })
153
157
  return
154
158
  }
155
159
  execution.start(target.id, pipeline)
@@ -204,14 +208,14 @@ async function onDrop(event: DragEvent) {
204
208
  >
205
209
  <UIcon name="i-lucide-layout-dashboard" class="h-10 w-10 text-slate-600" />
206
210
  <div>
207
- <h2 class="text-base font-semibold text-slate-300">Your board is empty</h2>
211
+ <h2 class="text-base font-semibold text-slate-300">{{ t('board.canvas.emptyTitle') }}</h2>
208
212
  <p class="mt-1 max-w-sm text-sm text-slate-500">
209
- Add a service to get started: bootstrap a fresh repo or pull in one you already have.
213
+ {{ t('board.canvas.emptyBody') }}
210
214
  </p>
211
215
  </div>
212
216
  <div class="pointer-events-auto flex flex-wrap items-center justify-center gap-2">
213
217
  <UButton color="primary" icon="i-lucide-git-branch-plus" @click="ui.openBootstrap()">
214
- Bootstrap repo
218
+ {{ t('nav.bootstrapRepo') }}
215
219
  </UButton>
216
220
  <UButton
217
221
  v-if="github.available"
@@ -220,7 +224,7 @@ async function onDrop(event: DragEvent) {
220
224
  icon="i-lucide-folder-git-2"
221
225
  @click="ui.openAddService()"
222
226
  >
223
- Add from existing repo
227
+ {{ t('nav.addFromRepo') }}
224
228
  </UButton>
225
229
  </div>
226
230
  </div>