@cat-factory/app 0.46.10 → 0.46.11

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 (29) hide show
  1. package/app/components/observability/StepMetricsBar.vue +31 -11
  2. package/app/components/observability/StepModelActivity.vue +3 -2
  3. package/app/components/panels/AgentStepDetail.vue +52 -39
  4. package/app/components/panels/DecisionModal.vue +12 -5
  5. package/app/components/panels/GenericStructuredResultView.vue +16 -6
  6. package/app/components/panels/InspectorPanel.vue +35 -28
  7. package/app/components/panels/ObservabilityPanel.vue +92 -49
  8. package/app/components/panels/StepMetadataCard.vue +87 -30
  9. package/app/components/panels/StepRestartControl.vue +4 -3
  10. package/app/components/panels/StepRunMeta.vue +23 -10
  11. package/app/components/panels/StepTestReport.vue +14 -11
  12. package/app/components/panels/inspector/ContainerSummary.vue +25 -14
  13. package/app/components/panels/inspector/EpicChildren.vue +7 -4
  14. package/app/components/panels/inspector/RecurringScheduleSettings.vue +60 -19
  15. package/app/components/panels/inspector/ServiceFragments.vue +3 -2
  16. package/app/components/panels/inspector/ServiceReleaseHealthConfig.vue +18 -13
  17. package/app/components/panels/inspector/ServiceTestConfig.vue +50 -39
  18. package/app/components/panels/inspector/TaskAgentConfig.vue +6 -5
  19. package/app/components/panels/inspector/TaskDependencies.vue +9 -4
  20. package/app/components/panels/inspector/TaskEstimateBadge.vue +14 -13
  21. package/app/components/panels/inspector/TaskExecution.vue +65 -31
  22. package/app/components/panels/inspector/TaskRunSettings.vue +93 -56
  23. package/app/components/panels/inspector/TaskStructure.vue +5 -4
  24. package/i18n/locales/en.json +402 -0
  25. package/i18n/locales/es.json +402 -0
  26. package/i18n/locales/fr.json +402 -0
  27. package/i18n/locales/pl.json +402 -0
  28. package/i18n/locales/uk.json +402 -0
  29. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { computed } from 'vue'
3
- import type { AgentState, PipelineStep, CompanionVerdict } from '~/types/execution'
3
+ import type { AgentState, PipelineStep, CompanionVerdict, StepApproval } from '~/types/execution'
4
4
  import { subtaskIconClass } from '~/utils/pipelineRender'
5
5
  import StepModelActivity from '~/components/observability/StepModelActivity.vue'
6
6
 
@@ -21,20 +21,28 @@ const props = defineProps<{
21
21
  }>()
22
22
 
23
23
  const models = useModelsStore()
24
+ const { t, d } = useI18n()
24
25
 
25
- const STATE_META: Record<AgentState, { label: string; color: string }> = {
26
- pending: { label: 'Pending', color: '#64748b' },
27
- working: { label: 'Working', color: '#6366f1' },
28
- waiting_decision: { label: 'Needs input', color: '#f59e0b' },
29
- done: { label: 'Done', color: '#22c55e' },
26
+ const STATE_LABEL_KEYS: Record<AgentState, string> = {
27
+ pending: 'panels.stepMeta.state.pending',
28
+ working: 'panels.stepMeta.state.working',
29
+ waiting_decision: 'panels.stepMeta.state.waiting_decision',
30
+ done: 'panels.stepMeta.state.done',
31
+ }
32
+ const STATE_COLOR: Record<AgentState, string> = {
33
+ pending: '#64748b',
34
+ working: '#6366f1',
35
+ waiting_decision: '#f59e0b',
36
+ done: '#22c55e',
30
37
  }
31
38
 
32
39
  // The state badge: a step left mid-flight on a failed run keeps `state: 'working'`,
33
40
  // so report it as "Failed" rather than the misleading "Working".
34
41
  const stateMeta = computed(() => {
35
42
  const s = props.step
36
- if (props.runFailed && s.state === 'working') return { label: 'Failed', color: '#ef4444' }
37
- return STATE_META[s.state]
43
+ if (props.runFailed && s.state === 'working')
44
+ return { label: t('panels.stepMeta.state.failed'), color: '#ef4444' }
45
+ return { label: t(STATE_LABEL_KEYS[s.state]), color: STATE_COLOR[s.state] }
38
46
  })
39
47
 
40
48
  const modelLabel = computed(() => (props.step.model ? models.labelForRef(props.step.model) : null))
@@ -47,8 +55,18 @@ const ITEM_ICON: Record<string, string> = {
47
55
 
48
56
  const pctOf = (n: number) => `${Math.round(n * 100)}%`
49
57
 
58
+ const APPROVAL_STATUS_KEYS: Record<StepApproval['status'], string> = {
59
+ pending: 'panels.stepMeta.approvalStatus.pending',
60
+ approved: 'panels.stepMeta.approvalStatus.approved',
61
+ changes_requested: 'panels.stepMeta.approvalStatus.changes_requested',
62
+ rejected: 'panels.stepMeta.approvalStatus.rejected',
63
+ }
64
+ const approvalStatusLabel = computed(() =>
65
+ props.step.approval ? t(APPROVAL_STATUS_KEYS[props.step.approval.status]) : '',
66
+ )
67
+
50
68
  function formatClock(ms?: number | null): string | null {
51
- return ms ? new Date(ms).toLocaleString() : null
69
+ return ms ? d(new Date(ms), 'long') : null
52
70
  }
53
71
 
54
72
  async function copyRunId() {
@@ -61,7 +79,9 @@ async function copyRunId() {
61
79
  <div>
62
80
  <dl class="grid grid-cols-2 gap-x-6 gap-y-3 text-[13px] sm:grid-cols-3">
63
81
  <div>
64
- <dt class="text-[11px] uppercase tracking-wide text-slate-500">State</dt>
82
+ <dt class="text-[11px] uppercase tracking-wide text-slate-500">
83
+ {{ t('panels.stepMeta.stateLabel') }}
84
+ </dt>
65
85
  <dd class="mt-0.5 flex items-center gap-1.5 text-slate-200">
66
86
  <UIcon
67
87
  v-if="runFailed && step.state === 'working'"
@@ -74,7 +94,9 @@ async function copyRunId() {
74
94
  </dd>
75
95
  </div>
76
96
  <div>
77
- <dt class="text-[11px] uppercase tracking-wide text-slate-500">Duration</dt>
97
+ <dt class="text-[11px] uppercase tracking-wide text-slate-500">
98
+ {{ t('panels.stepMeta.duration') }}
99
+ </dt>
78
100
  <dd class="mt-0.5 flex items-center gap-1.5 tabular-nums text-slate-200">
79
101
  <UIcon
80
102
  v-if="isRunning"
@@ -83,33 +105,47 @@ async function copyRunId() {
83
105
  />
84
106
  <span v-if="durationLabel">{{ durationLabel }}</span>
85
107
  <span v-else class="text-slate-500">—</span>
86
- <span v-if="isRunning" class="text-[11px] text-slate-500">elapsed</span>
108
+ <span v-if="isRunning" class="text-[11px] text-slate-500">{{
109
+ t('panels.stepMeta.elapsed')
110
+ }}</span>
87
111
  </dd>
88
112
  </div>
89
113
  <div>
90
- <dt class="text-[11px] uppercase tracking-wide text-slate-500">Step</dt>
91
- <dd class="mt-0.5 text-slate-200">{{ stepNumber }} of {{ totalSteps }}</dd>
114
+ <dt class="text-[11px] uppercase tracking-wide text-slate-500">
115
+ {{ t('panels.stepMeta.step') }}
116
+ </dt>
117
+ <dd class="mt-0.5 text-slate-200">
118
+ {{ t('panels.stepMeta.stepOf', { number: stepNumber, total: totalSteps }) }}
119
+ </dd>
92
120
  </div>
93
121
  <div>
94
- <dt class="text-[11px] uppercase tracking-wide text-slate-500">Started</dt>
122
+ <dt class="text-[11px] uppercase tracking-wide text-slate-500">
123
+ {{ t('panels.stepMeta.started') }}
124
+ </dt>
95
125
  <dd class="mt-0.5 text-slate-300">{{ formatClock(step.startedAt) ?? '—' }}</dd>
96
126
  </div>
97
127
  <div>
98
- <dt class="text-[11px] uppercase tracking-wide text-slate-500">Finished</dt>
128
+ <dt class="text-[11px] uppercase tracking-wide text-slate-500">
129
+ {{ t('panels.stepMeta.finished') }}
130
+ </dt>
99
131
  <dd class="mt-0.5 text-slate-300">{{ formatClock(step.finishedAt) ?? '—' }}</dd>
100
132
  </div>
101
133
  <div>
102
- <dt class="text-[11px] uppercase tracking-wide text-slate-500">Model</dt>
134
+ <dt class="text-[11px] uppercase tracking-wide text-slate-500">
135
+ {{ t('panels.stepMeta.model') }}
136
+ </dt>
103
137
  <dd class="mt-0.5 truncate text-slate-300" :title="step.model">
104
- {{ modelLabel ?? 'Not recorded' }}
138
+ {{ modelLabel ?? t('panels.stepMeta.notRecorded') }}
105
139
  </dd>
106
140
  </div>
107
141
  <!-- The run id this step belongs to, surfaced for debugging (copyable). -->
108
142
  <div class="col-span-2 sm:col-span-3">
109
- <dt class="text-[11px] uppercase tracking-wide text-slate-500">Run</dt>
143
+ <dt class="text-[11px] uppercase tracking-wide text-slate-500">
144
+ {{ t('panels.stepMeta.run') }}
145
+ </dt>
110
146
  <dd
111
147
  class="mt-0.5 cursor-pointer truncate font-mono text-[12px] text-slate-400 hover:text-slate-200"
112
- :title="`${step.runId ?? instanceId ?? ''} — click to copy`"
148
+ :title="t('panels.stepMeta.clickToCopy', { id: step.runId ?? instanceId ?? '' })"
113
149
  @click="copyRunId"
114
150
  >
115
151
  {{ step.runId ?? instanceId ?? '—' }}
@@ -124,13 +160,18 @@ async function copyRunId() {
124
160
  class="mt-4 flex items-center gap-2 rounded-lg border border-sky-900/50 bg-sky-950/30 px-3 py-2 text-[12px] text-sky-300"
125
161
  >
126
162
  <UIcon name="i-lucide-loader-circle" class="h-4 w-4 shrink-0 animate-spin" />
127
- <span>Spinning up container…</span>
163
+ <span>{{ t('panels.stepMeta.spinningUpContainer') }}</span>
128
164
  </div>
129
165
 
130
166
  <!-- live subtask breakdown -->
131
167
  <div v-if="step.subtasks && step.subtasks.total > 0" class="mt-4">
132
168
  <div class="text-[11px] uppercase tracking-wide text-slate-500">
133
- Subtasks · {{ step.subtasks.completed }}/{{ step.subtasks.total }}
169
+ {{
170
+ t('panels.stepMeta.subtasks', {
171
+ completed: step.subtasks.completed,
172
+ total: step.subtasks.total,
173
+ })
174
+ }}
134
175
  </div>
135
176
  <div class="mt-1 h-1 overflow-hidden rounded-full bg-slate-700/60">
136
177
  <div
@@ -170,7 +211,9 @@ async function copyRunId() {
170
211
 
171
212
  <!-- standards (prompt fragments) folded into this step -->
172
213
  <div v-if="step.selectedFragmentIds && step.selectedFragmentIds.length" class="mt-4">
173
- <div class="text-[11px] uppercase tracking-wide text-slate-500">Standards applied</div>
214
+ <div class="text-[11px] uppercase tracking-wide text-slate-500">
215
+ {{ t('panels.stepMeta.standardsApplied') }}
216
+ </div>
174
217
  <div class="mt-1 flex flex-wrap gap-1">
175
218
  <UBadge
176
219
  v-for="id in step.selectedFragmentIds"
@@ -186,7 +229,9 @@ async function copyRunId() {
186
229
 
187
230
  <!-- decision raised on this step -->
188
231
  <div v-if="step.decision" class="mt-4">
189
- <div class="text-[11px] uppercase tracking-wide text-slate-500">Decision</div>
232
+ <div class="text-[11px] uppercase tracking-wide text-slate-500">
233
+ {{ t('panels.stepMeta.decision') }}
234
+ </div>
190
235
  <p class="mt-0.5 text-[13px] text-slate-200">{{ step.decision.question }}</p>
191
236
  <p
192
237
  v-if="step.decision.chosen"
@@ -195,21 +240,27 @@ async function copyRunId() {
195
240
  <UIcon name="i-lucide-check" class="h-3 w-3 shrink-0" />
196
241
  {{ step.decision.chosen }}
197
242
  </p>
198
- <p v-else class="mt-0.5 text-[12px] text-amber-400">Awaiting a human choice</p>
243
+ <p v-else class="mt-0.5 text-[12px] text-amber-400">
244
+ {{ t('panels.stepMeta.awaitingChoice') }}
245
+ </p>
199
246
  </div>
200
247
 
201
248
  <!-- approval gate state -->
202
249
  <div v-if="step.approval" class="mt-4">
203
- <div class="text-[11px] uppercase tracking-wide text-slate-500">Approval gate</div>
204
- <p class="mt-0.5 text-[13px] text-slate-200 capitalize">
205
- {{ step.approval.status.replace('_', ' ') }}
250
+ <div class="text-[11px] uppercase tracking-wide text-slate-500">
251
+ {{ t('panels.stepMeta.approvalGate') }}
252
+ </div>
253
+ <p class="mt-0.5 text-[13px] text-slate-200">
254
+ {{ approvalStatusLabel }}
206
255
  </p>
207
256
  </div>
208
257
 
209
258
  <!-- companion verdict + full correction sequence -->
210
259
  <div v-if="companionVerdicts.length" class="mt-4">
211
260
  <div class="flex items-center justify-between">
212
- <span class="text-[11px] uppercase tracking-wide text-slate-500"> Companion review </span>
261
+ <span class="text-[11px] uppercase tracking-wide text-slate-500">
262
+ {{ t('panels.stepMeta.companionReview') }}
263
+ </span>
213
264
  <UBadge :color="latestVerdict?.passed ? 'success' : 'warning'" variant="subtle" size="sm">
214
265
  {{ pctOf(latestVerdict!.rating) }}
215
266
  {{ latestVerdict?.passed ? '≥' : '<' }} {{ pctOf(latestVerdict!.threshold) }}
@@ -234,7 +285,13 @@ async function copyRunId() {
234
285
  </li>
235
286
  </ol>
236
287
  <p v-if="companionVerdicts.length > 1" class="mt-1 text-[11px] text-slate-500">
237
- {{ companionVerdicts.length }} correction iteration(s).
288
+ {{
289
+ t(
290
+ 'panels.stepMeta.correctionIterations',
291
+ { count: companionVerdicts.length },
292
+ companionVerdicts.length,
293
+ )
294
+ }}
238
295
  </p>
239
296
  </div>
240
297
  </div>
@@ -25,6 +25,7 @@ const props = defineProps<{
25
25
  const emit = defineEmits<{ restarted: [] }>()
26
26
 
27
27
  const execution = useExecutionStore()
28
+ const { t } = useI18n()
28
29
 
29
30
  const instance = computed(() =>
30
31
  props.instanceId ? execution.getInstance(props.instanceId) : undefined,
@@ -62,7 +63,7 @@ async function restart() {
62
63
  color="neutral"
63
64
  variant="ghost"
64
65
  size="sm"
65
- title="Restart pipeline from this step"
66
+ :title="t('panels.stepRestart.restartFromStep')"
66
67
  @click="armed = true"
67
68
  />
68
69
  <template v-else>
@@ -74,7 +75,7 @@ async function restart() {
74
75
  :loading="restarting"
75
76
  @click="restart"
76
77
  >
77
- Restart from here
78
+ {{ t('panels.stepRestart.restartFromHere') }}
78
79
  </UButton>
79
80
  <UButton
80
81
  color="neutral"
@@ -83,7 +84,7 @@ async function restart() {
83
84
  :disabled="restarting"
84
85
  @click="armed = false"
85
86
  >
86
- Cancel
87
+ {{ t('common.cancel') }}
87
88
  </UButton>
88
89
  </template>
89
90
  </template>
@@ -25,6 +25,7 @@ const props = defineProps<{
25
25
  }>()
26
26
 
27
27
  const models = useModelsStore()
28
+ const { t, d } = useI18n()
28
29
 
29
30
  const { isRunning, durationLabel } = useStepTimer({
30
31
  step: () => props.step,
@@ -36,7 +37,7 @@ const modelLabel = computed(() => (props.step.model ? models.labelForRef(props.s
36
37
  const runId = computed(() => props.step.runId ?? props.instanceId ?? null)
37
38
 
38
39
  function formatClock(ms?: number | null): string | null {
39
- return ms ? new Date(ms).toLocaleString() : null
40
+ return ms ? d(new Date(ms), 'long') : null
40
41
  }
41
42
 
42
43
  async function copyRunId() {
@@ -47,13 +48,17 @@ async function copyRunId() {
47
48
  <template>
48
49
  <div class="flex flex-col gap-4">
49
50
  <div v-if="stepNumber && totalSteps">
50
- <h4 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">Step</h4>
51
- <p class="text-[12px] text-slate-300">{{ stepNumber }} of {{ totalSteps }}</p>
51
+ <h4 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
52
+ {{ t('panels.stepMeta.step') }}
53
+ </h4>
54
+ <p class="text-[12px] text-slate-300">
55
+ {{ t('panels.stepMeta.stepOf', { number: stepNumber, total: totalSteps }) }}
56
+ </p>
52
57
  </div>
53
58
 
54
59
  <div v-if="durationLabel">
55
60
  <h4 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
56
- Duration
61
+ {{ t('panels.stepMeta.duration') }}
57
62
  </h4>
58
63
  <p class="flex items-center gap-1.5 text-[12px] tabular-nums text-slate-300">
59
64
  <UIcon
@@ -62,34 +67,42 @@ async function copyRunId() {
62
67
  class="h-3 w-3 animate-spin text-indigo-400"
63
68
  />
64
69
  {{ durationLabel }}
65
- <span v-if="isRunning" class="text-[11px] text-slate-500">elapsed</span>
70
+ <span v-if="isRunning" class="text-[11px] text-slate-500">{{
71
+ t('panels.stepMeta.elapsed')
72
+ }}</span>
66
73
  </p>
67
74
  </div>
68
75
 
69
76
  <div v-if="formatClock(step.startedAt)">
70
- <h4 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">Started</h4>
77
+ <h4 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
78
+ {{ t('panels.stepMeta.started') }}
79
+ </h4>
71
80
  <p class="text-[12px] text-slate-300">{{ formatClock(step.startedAt) }}</p>
72
81
  </div>
73
82
 
74
83
  <div v-if="formatClock(step.finishedAt)">
75
84
  <h4 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
76
- Finished
85
+ {{ t('panels.stepMeta.finished') }}
77
86
  </h4>
78
87
  <p class="text-[12px] text-slate-300">{{ formatClock(step.finishedAt) }}</p>
79
88
  </div>
80
89
 
81
90
  <div v-if="step.model">
82
- <h4 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">Model</h4>
91
+ <h4 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
92
+ {{ t('panels.stepMeta.model') }}
93
+ </h4>
83
94
  <p class="break-all text-[12px] text-slate-300" :title="step.model">
84
95
  {{ modelLabel ?? step.model }}
85
96
  </p>
86
97
  </div>
87
98
 
88
99
  <div v-if="runId">
89
- <h4 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">Run</h4>
100
+ <h4 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
101
+ {{ t('panels.stepMeta.run') }}
102
+ </h4>
90
103
  <p
91
104
  class="cursor-pointer break-all font-mono text-[12px] text-slate-400 hover:text-slate-200"
92
- :title="`${runId} click to copy`"
105
+ :title="t('panels.stepMeta.clickToCopy', { id: runId })"
93
106
  @click="copyRunId"
94
107
  >
95
108
  {{ runId }}
@@ -9,6 +9,8 @@ defineProps<{
9
9
  phase: TesterStepState | null
10
10
  }>()
11
11
 
12
+ const { t } = useI18n()
13
+
12
14
  const SEVERITY_COLOR: Record<string, string> = {
13
15
  low: '#64748b',
14
16
  medium: '#f59e0b',
@@ -26,16 +28,17 @@ const OUTCOME_COLOR: Record<string, string> = {
26
28
  <section class="mt-4 scroll-mt-4">
27
29
  <div class="mb-2 flex items-center gap-1.5 text-[11px]">
28
30
  <UIcon name="i-lucide-flask-conical" class="h-3.5 w-3.5 text-slate-400" />
29
- <span class="font-semibold uppercase tracking-wide text-slate-400"> Test report </span>
31
+ <span class="font-semibold uppercase tracking-wide text-slate-400">
32
+ {{ t('panels.testReport.title') }}
33
+ </span>
30
34
  <UBadge :color="report.greenlight ? 'success' : 'warning'" variant="subtle" size="sm">
31
- {{ report.greenlight ? 'Greenlit' : 'Needs fixes' }}
35
+ {{
36
+ report.greenlight ? t('panels.testReport.greenlit') : t('panels.testReport.needsFixes')
37
+ }}
32
38
  </UBadge>
33
39
  <span v-if="phase && phase.attempts > 0" class="text-[11px] text-slate-500">
34
- {{ phase.attempts }}/{{ phase.maxAttempts }} fix attempt(s)<span
35
- v-if="phase.phase === 'fixing'"
36
- >
37
- · fixing…</span
38
- >
40
+ {{ t('panels.testReport.fixAttempts', { count: phase.attempts, max: phase.maxAttempts })
41
+ }}<span v-if="phase.phase === 'fixing'"> {{ t('panels.testReport.fixing') }}</span>
39
42
  </span>
40
43
  </div>
41
44
  <p v-if="report.summary" class="mb-3 text-[13px] leading-relaxed text-slate-300">
@@ -43,14 +46,14 @@ const OUTCOME_COLOR: Record<string, string> = {
43
46
  </p>
44
47
 
45
48
  <div v-if="report.tested.length" class="mb-3">
46
- <div class="mb-1 text-[11px] text-slate-500">Tested</div>
49
+ <div class="mb-1 text-[11px] text-slate-500">{{ t('panels.testReport.tested') }}</div>
47
50
  <ul class="space-y-0.5 text-[12px] text-slate-300">
48
- <li v-for="(t, i) in report.tested" :key="i">• {{ t }}</li>
51
+ <li v-for="(item, i) in report.tested" :key="i">• {{ item }}</li>
49
52
  </ul>
50
53
  </div>
51
54
 
52
55
  <div v-if="report.outcomes.length" class="mb-3 space-y-1">
53
- <div class="text-[11px] text-slate-500">Outcomes</div>
56
+ <div class="text-[11px] text-slate-500">{{ t('panels.testReport.outcomes') }}</div>
54
57
  <div v-for="(o, i) in report.outcomes" :key="i" class="flex items-start gap-2 text-[12px]">
55
58
  <span
56
59
  class="mt-1 h-2 w-2 shrink-0 rounded-full"
@@ -63,7 +66,7 @@ const OUTCOME_COLOR: Record<string, string> = {
63
66
  </div>
64
67
 
65
68
  <div v-if="report.concerns.length" class="space-y-1">
66
- <div class="text-[11px] text-slate-500">Concerns</div>
69
+ <div class="text-[11px] text-slate-500">{{ t('panels.testReport.concerns') }}</div>
67
70
  <div
68
71
  v-for="(c, i) in report.concerns"
69
72
  :key="i"
@@ -6,6 +6,7 @@ const props = defineProps<{ block: Block }>()
6
6
 
7
7
  const board = useBoardStore()
8
8
  const ui = useUiStore()
9
+ const { t } = useI18n()
9
10
 
10
11
  const isFrame = computed(() => (props.block.level ?? 'frame') === 'frame')
11
12
  const modules = computed(() => (isFrame.value ? board.modulesOf(props.block.id) : []))
@@ -23,7 +24,7 @@ function addTask() {
23
24
  <!-- modules (services only) -->
24
25
  <div v-if="modules.length">
25
26
  <div class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
26
- Modules ({{ modules.length }})
27
+ {{ t('inspector.container.modules', { count: modules.length }) }}
27
28
  </div>
28
29
  <ul class="space-y-1">
29
30
  <li
@@ -34,9 +35,13 @@ function addTask() {
34
35
  >
35
36
  <UIcon name="i-lucide-package" class="h-3.5 w-3.5 text-violet-400" />
36
37
  <span class="truncate text-xs text-slate-200">{{ m.title }}</span>
37
- <span class="ml-auto text-[10px] text-slate-500"
38
- >{{ board.tasksOf(m.id).length }} task(s)</span
39
- >
38
+ <span class="ml-auto text-[10px] text-slate-500">{{
39
+ t(
40
+ 'inspector.container.taskCount',
41
+ { count: board.tasksOf(m.id).length },
42
+ board.tasksOf(m.id).length,
43
+ )
44
+ }}</span>
40
45
  </li>
41
46
  </ul>
42
47
  </div>
@@ -44,31 +49,37 @@ function addTask() {
44
49
  <div>
45
50
  <div class="mb-1 flex items-center justify-between">
46
51
  <span class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
47
- {{ isFrame ? 'All tasks' : 'Tasks' }} ({{ tasks.length }})
52
+ {{
53
+ isFrame
54
+ ? t('inspector.container.allTasks', { count: tasks.length })
55
+ : t('inspector.container.tasks', { count: tasks.length })
56
+ }}
48
57
  </span>
49
58
  <UButton size="xs" variant="soft" color="primary" icon="i-lucide-plus" @click="addTask">
50
- Add task
59
+ {{ t('inspector.container.addTask') }}
51
60
  </UButton>
52
61
  </div>
53
62
  <ul v-if="tasks.length" class="space-y-1">
54
63
  <li
55
- v-for="t in tasks"
56
- :key="t.id"
64
+ v-for="task in tasks"
65
+ :key="task.id"
57
66
  class="flex cursor-pointer items-center gap-2 rounded-md px-2 py-1 hover:bg-slate-800/60"
58
- @click="ui.select(t.id)"
67
+ @click="ui.select(task.id)"
59
68
  >
60
69
  <span
61
70
  class="h-2 w-2 shrink-0 rounded-full"
62
- :style="{ backgroundColor: STATUS_META[t.status].color }"
71
+ :style="{ backgroundColor: STATUS_META[task.status].color }"
63
72
  />
64
- <span class="truncate text-xs text-slate-200">{{ t.title }}</span>
65
- <span class="ml-auto text-[10px] text-slate-500">{{ STATUS_META[t.status].label }}</span>
73
+ <span class="truncate text-xs text-slate-200">{{ task.title }}</span>
74
+ <span class="ml-auto text-[10px] text-slate-500">{{
75
+ STATUS_META[task.status].label
76
+ }}</span>
66
77
  </li>
67
78
  </ul>
68
- <div v-else class="text-[11px] text-slate-500">No tasks yet — add one to start work.</div>
79
+ <div v-else class="text-[11px] text-slate-500">{{ t('inspector.container.noTasks') }}</div>
69
80
  </div>
70
81
  <p v-if="isFrame" class="text-[11px] text-slate-500">
71
- Services are long-lived — they don't "complete". Work happens in their tasks &amp; modules.
82
+ {{ t('inspector.container.servicesHint') }}
72
83
  </p>
73
84
  </div>
74
85
  </template>
@@ -10,6 +10,7 @@ const props = defineProps<{ block: Block }>()
10
10
 
11
11
  const board = useBoardStore()
12
12
  const ui = useUiStore()
13
+ const { t } = useI18n()
13
14
 
14
15
  const members = computed(() => board.epicMembers(props.block.id))
15
16
  const done = computed(() => members.value.filter((m) => m.status === 'done').length)
@@ -47,13 +48,15 @@ const groups = computed(() => {
47
48
  <div>
48
49
  <div class="mb-1 flex items-center justify-between">
49
50
  <span class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
50
- Epic tasks
51
+ {{ t('inspector.epicChildren.title') }}
51
52
  </span>
52
- <span class="text-[11px] text-slate-500">{{ done }}/{{ members.length }} done</span>
53
+ <span class="text-[11px] text-slate-500">{{
54
+ t('inspector.epicChildren.doneCount', { done, total: members.length })
55
+ }}</span>
53
56
  </div>
54
57
 
55
58
  <div v-if="members.length === 0" class="text-[11px] text-slate-500">
56
- No tasks belong to this epic yet. Import an epic's children, or set a task's epic.
59
+ {{ t('inspector.epicChildren.empty') }}
57
60
  </div>
58
61
 
59
62
  <div v-else class="space-y-2">
@@ -64,7 +67,7 @@ const groups = computed(() => {
64
67
  >
65
68
  <div class="mb-1 flex items-center gap-1 text-[11px] font-medium text-slate-300">
66
69
  <UIcon name="i-lucide-box" class="h-3 w-3 text-slate-500" />
67
- {{ group.service?.title ?? 'Unassigned' }}
70
+ {{ group.service?.title ?? t('inspector.epicChildren.unassigned') }}
68
71
  </div>
69
72
  <div v-for="(mod, mi) in [...group.modules.values()]" :key="mi" class="pl-1">
70
73
  <div v-if="mod.module" class="text-[10px] uppercase tracking-wide text-slate-500">