@cat-factory/app 0.46.9 → 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 (42) hide show
  1. package/app/components/board/AddTaskModal.vue +106 -88
  2. package/app/components/board/AgentFailureCard.vue +8 -5
  3. package/app/components/board/AgentStopButton.vue +8 -5
  4. package/app/components/board/BoardCanvas.vue +13 -9
  5. package/app/components/board/RecurringPipelineModal.vue +33 -23
  6. package/app/components/board/nodes/BlockNode.vue +50 -34
  7. package/app/components/board/nodes/DecisionBadge.vue +6 -3
  8. package/app/components/board/nodes/DraggableTask.vue +2 -1
  9. package/app/components/board/nodes/EpicNode.vue +10 -3
  10. package/app/components/board/nodes/ModuleFrame.vue +6 -5
  11. package/app/components/board/nodes/TaskCard.vue +40 -22
  12. package/app/components/board/nodes/TaskPipelineMini.vue +4 -3
  13. package/app/components/layout/BoardToolbar.vue +26 -19
  14. package/app/components/observability/StepMetricsBar.vue +31 -11
  15. package/app/components/observability/StepModelActivity.vue +3 -2
  16. package/app/components/panels/AgentStepDetail.vue +52 -39
  17. package/app/components/panels/DecisionModal.vue +12 -5
  18. package/app/components/panels/GenericStructuredResultView.vue +16 -6
  19. package/app/components/panels/InspectorPanel.vue +35 -28
  20. package/app/components/panels/ObservabilityPanel.vue +92 -49
  21. package/app/components/panels/StepMetadataCard.vue +87 -30
  22. package/app/components/panels/StepRestartControl.vue +4 -3
  23. package/app/components/panels/StepRunMeta.vue +23 -10
  24. package/app/components/panels/StepTestReport.vue +14 -11
  25. package/app/components/panels/inspector/ContainerSummary.vue +25 -14
  26. package/app/components/panels/inspector/EpicChildren.vue +7 -4
  27. package/app/components/panels/inspector/RecurringScheduleSettings.vue +60 -19
  28. package/app/components/panels/inspector/ServiceFragments.vue +3 -2
  29. package/app/components/panels/inspector/ServiceReleaseHealthConfig.vue +18 -13
  30. package/app/components/panels/inspector/ServiceTestConfig.vue +50 -39
  31. package/app/components/panels/inspector/TaskAgentConfig.vue +6 -5
  32. package/app/components/panels/inspector/TaskDependencies.vue +9 -4
  33. package/app/components/panels/inspector/TaskEstimateBadge.vue +14 -13
  34. package/app/components/panels/inspector/TaskExecution.vue +65 -31
  35. package/app/components/panels/inspector/TaskRunSettings.vue +93 -56
  36. package/app/components/panels/inspector/TaskStructure.vue +5 -4
  37. package/i18n/locales/en.json +606 -0
  38. package/i18n/locales/es.json +596 -2
  39. package/i18n/locales/fr.json +596 -2
  40. package/i18n/locales/pl.json +596 -2
  41. package/i18n/locales/uk.json +596 -2
  42. package/package.json +1 -1
@@ -13,6 +13,7 @@ const ui = useUiStore()
13
13
  const agentRuns = useAgentRunsStore()
14
14
  const reviews = useReviewStage()
15
15
  const toast = useToast()
16
+ const { t } = useI18n()
16
17
 
17
18
  const task = computed<Block | undefined>(() => board.getBlock(props.taskId))
18
19
  const statusMeta = computed(() => (task.value ? STATUS_META[task.value.status] : null))
@@ -43,7 +44,9 @@ const defaultPipeline = computed(
43
44
 
44
45
  /** The PR the implementer agent opened for this task, if any. */
45
46
  const pr = computed(() => task.value?.pullRequest)
46
- const prLabel = computed(() => (pr.value?.number ? `PR #${pr.value.number}` : 'PR'))
47
+ const prLabel = computed(() =>
48
+ pr.value?.number ? t('board.task.prNumber', { number: pr.value.number }) : t('board.task.pr'),
49
+ )
47
50
 
48
51
  // This task's current agent run (if any). A failed run must surface the shared
49
52
  // failure banner + retry — NOT a stuck progress bar — so the card never looks
@@ -65,15 +68,18 @@ const starting = ref(false)
65
68
  async function run() {
66
69
  if (!runnable.value) {
67
70
  toast.add({
68
- title: 'Blocked by dependencies',
69
- description: `Waiting on: ${unmet.value.map((d) => d.title).join(', ')}`,
71
+ title: t('board.task.blockedByDependenciesTitle'),
72
+ description: t('board.task.waitingOn', { deps: unmet.value.map((d) => d.title).join(', ') }),
70
73
  icon: 'i-lucide-lock',
71
74
  })
72
75
  return
73
76
  }
74
77
  const pipeline = defaultPipeline.value
75
78
  if (!pipeline) {
76
- toast.add({ title: 'No pipeline defined', description: 'Create one in the builder first.' })
79
+ toast.add({
80
+ title: t('board.task.noPipelineTitle'),
81
+ description: t('board.task.noPipelineBody'),
82
+ })
77
83
  return
78
84
  }
79
85
  starting.value = true
@@ -108,11 +114,11 @@ const pendingDecision = computed(() =>
108
114
  const reviewStage = computed(() => reviews.stageForBlock(props.taskId))
109
115
  const reviewStageLabel = computed(() =>
110
116
  reviewStage.value === 'incorporating'
111
- ? 'Incorporating answers…'
117
+ ? t('board.task.incorporatingAnswers')
112
118
  : reviewStage.value === 'reviewing'
113
- ? 'Re-reviewing…'
119
+ ? t('board.task.reReviewing')
114
120
  : reviewStage.value === 'recommending'
115
- ? 'Recommending…'
121
+ ? t('board.task.recommending')
116
122
  : null,
117
123
  )
118
124
  const pendingApproval = computed(() => {
@@ -135,17 +141,17 @@ const attention = computed<{
135
141
  const d = pendingDecision.value
136
142
  if (d)
137
143
  return {
138
- label: 'Decision needed',
144
+ label: t('board.task.decisionNeeded'),
139
145
  icon: 'i-lucide-circle-help',
140
- action: 'Resolve',
146
+ action: t('board.task.resolve'),
141
147
  open: () => ui.openDecision(d.instanceId, d.decision.id),
142
148
  }
143
149
  const a = pendingApproval.value
144
150
  if (a)
145
151
  return {
146
- label: 'Approval needed',
152
+ label: t('board.task.approvalNeeded'),
147
153
  icon: 'i-lucide-shield-check',
148
- action: 'Approve',
154
+ action: t('board.task.approve'),
149
155
  open: () => ui.openApprovalDetail(a.instanceId, a.approval.id),
150
156
  }
151
157
  return null
@@ -155,7 +161,7 @@ const attention = computed<{
155
161
  * decision/approval reason, otherwise the generic status label. */
156
162
  const statusText = computed(() =>
157
163
  runFailed.value
158
- ? 'Failed'
164
+ ? t('board.task.failed')
159
165
  : (reviewStageLabel.value ?? attention.value?.label ?? statusMeta.value?.label ?? ''),
160
166
  )
161
167
 
@@ -191,7 +197,11 @@ function selectTask() {
191
197
  v-if="schedule"
192
198
  name="i-lucide-repeat"
193
199
  class="h-3 w-3 shrink-0 text-indigo-400"
194
- :title="schedule.enabled ? 'Recurring pipeline' : 'Recurring pipeline (paused)'"
200
+ :title="
201
+ schedule.enabled
202
+ ? t('board.task.recurringPipeline')
203
+ : t('board.task.recurringPipelinePaused')
204
+ "
195
205
  />
196
206
  <span
197
207
  class="ml-auto truncate text-[9px] uppercase tracking-wide"
@@ -211,7 +221,7 @@ function selectTask() {
211
221
  <button
212
222
  type="button"
213
223
  class="nodrag shrink-0 cursor-crosshair touch-none rounded-full p-0.5 text-slate-500 hover:bg-slate-800 hover:text-amber-400 pointer-coarse:p-2.5"
214
- title="Drag onto another task to make it depend on this one"
224
+ :title="t('board.task.dragToConnect')"
215
225
  @pointerdown.stop="startConnect(task.id, $event)"
216
226
  @click.stop
217
227
  >
@@ -302,12 +312,20 @@ function selectTask() {
302
312
  :disabled="!runnable || starting"
303
313
  :title="
304
314
  runnable
305
- ? `Start ${defaultPipeline?.name ?? 'pipeline'}`
306
- : `Waiting on: ${unmet.map((d) => d.title).join(', ')}`
315
+ ? t('board.task.startPipeline', {
316
+ name: defaultPipeline?.name ?? t('board.task.pipelineFallback'),
317
+ })
318
+ : t('board.task.waitingOn', { deps: unmet.map((d) => d.title).join(', ') })
307
319
  "
308
320
  @click.stop="run"
309
321
  >
310
- {{ starting ? 'Starting…' : runnable ? 'Start' : 'Blocked' }}
322
+ {{
323
+ starting
324
+ ? t('board.task.starting')
325
+ : runnable
326
+ ? t('board.task.start')
327
+ : t('board.task.blocked')
328
+ }}
311
329
  </UButton>
312
330
  <span
313
331
  v-if="runnable && defaultPipeline"
@@ -328,7 +346,7 @@ function selectTask() {
328
346
  variant="soft"
329
347
  size="xs"
330
348
  icon="i-lucide-git-pull-request"
331
- :title="`Open ${prLabel} on GitHub`"
349
+ :title="t('board.task.openPrOnGithub', { pr: prLabel })"
332
350
  @click.stop
333
351
  >
334
352
  {{ prLabel }}
@@ -340,7 +358,7 @@ function selectTask() {
340
358
  icon="i-lucide-scan-eye"
341
359
  @click.stop="review"
342
360
  >
343
- Review
361
+ {{ t('board.task.review') }}
344
362
  </UButton>
345
363
  <UButton
346
364
  color="success"
@@ -349,7 +367,7 @@ function selectTask() {
349
367
  icon="i-lucide-git-merge"
350
368
  @click.stop="merge"
351
369
  >
352
- Merge
370
+ {{ t('board.task.merge') }}
353
371
  </UButton>
354
372
  </template>
355
373
 
@@ -357,7 +375,7 @@ function selectTask() {
357
375
  v-else-if="task.status === 'done'"
358
376
  class="inline-flex items-center gap-1 text-[9px] text-emerald-400"
359
377
  >
360
- <UIcon name="i-lucide-check-check" class="h-3 w-3" /> implemented
378
+ <UIcon name="i-lucide-check-check" class="h-3 w-3" /> {{ t('board.task.implemented') }}
361
379
  </span>
362
380
  </div>
363
381
 
@@ -368,7 +386,7 @@ function selectTask() {
368
386
  >
369
387
  <span
370
388
  class="inline-flex items-center gap-1 rounded bg-violet-500/15 px-1.5 py-0.5 text-[9px] text-violet-200"
371
- :title="`Module: ${task.moduleName}`"
389
+ :title="t('board.task.module', { name: task.moduleName })"
372
390
  >
373
391
  <UIcon :name="MODULE_META.icon" class="h-3 w-3" :style="{ color: MODULE_META.color }" />
374
392
  {{ task.moduleName }}
@@ -21,6 +21,7 @@ const execution = useExecutionStore()
21
21
  const ui = useUiStore()
22
22
  const expansion = useTaskExpansionStore()
23
23
  const reviews = useReviewStage()
24
+ const { t } = useI18n()
24
25
  const { lod } = useSemanticZoom()
25
26
 
26
27
  const instance = computed(() => execution.getByBlock(props.taskId))
@@ -75,12 +76,12 @@ const ITEM_ICON: Record<string, string> = {
75
76
  <div v-if="showSteps" class="mt-2 space-y-1 border-t border-slate-800 pt-2">
76
77
  <div class="flex items-center gap-1 text-[9px] uppercase tracking-wide text-slate-500">
77
78
  <UIcon name="i-lucide-workflow" class="h-2.5 w-2.5" />
78
- Build steps
79
+ {{ t('board.task.buildSteps') }}
79
80
  </div>
80
81
  <div v-for="(s, i) in steps" :key="i" class="rounded bg-slate-900/60 px-1.5 py-1">
81
82
  <div
82
83
  class="flex cursor-pointer items-center gap-1"
83
- :title="`${agentKindMeta(s.agentKind).label} — ${agentKindMeta(s.agentKind).description}\nClick to view step details & output`"
84
+ :title="`${agentKindMeta(s.agentKind).label} — ${agentKindMeta(s.agentKind).description}\n${t('board.task.clickToViewStep')}`"
84
85
  @click.stop="openStep(i)"
85
86
  >
86
87
  <UIcon
@@ -131,7 +132,7 @@ const ITEM_ICON: Record<string, string> = {
131
132
  @click.stop="ui.openApprovalDetail(instance.id, s.approval.id)"
132
133
  >
133
134
  <UIcon name="i-lucide-shield-check" class="h-2.5 w-2.5" />
134
- Review &amp; approve
135
+ {{ t('board.task.reviewAndApprove') }}
135
136
  </button>
136
137
 
137
138
  <!-- per-step subtask progress bar -->
@@ -8,15 +8,20 @@ const execution = useExecutionStore()
8
8
  const workspace = useWorkspaceStore()
9
9
  const services = useServicesStore()
10
10
  const toast = useToast()
11
+ const { t, n } = useI18n()
11
12
  const { fitView, zoomIn, zoomOut } = useBoardFlow()
12
13
 
13
14
  async function mountService(serviceId: string, title: string) {
14
15
  try {
15
16
  await services.mount(serviceId)
16
- toast.add({ title: `Added ${title}`, icon: 'i-lucide-box', color: 'success' })
17
+ toast.add({
18
+ title: t('board.toolbar.serviceAdded', { title }),
19
+ icon: 'i-lucide-box',
20
+ color: 'success',
21
+ })
17
22
  } catch (e) {
18
23
  toast.add({
19
- title: 'Could not add service',
24
+ title: t('board.toolbar.serviceAddFailed'),
20
25
  description: e instanceof Error ? e.message : String(e),
21
26
  color: 'error',
22
27
  })
@@ -38,16 +43,16 @@ const mountableItems = computed(() =>
38
43
  )
39
44
 
40
45
  const zoomPct = computed(() => Math.round(ui.zoom * 100))
41
- const lodLabel = computed(
42
- () =>
43
- ({
44
- far: 'Overview',
45
- mid: 'Summary',
46
- close: 'Detail',
47
- steps: 'Build steps',
48
- subtasks: 'Subtasks',
49
- })[ui.lod],
50
- )
46
+ // Exhaustive (tier-2) map from level-of-detail → its label key, so adding an LOD
47
+ // without a label fails the typecheck rather than leaking a raw key.
48
+ const LOD_LABEL_KEYS = {
49
+ far: 'board.toolbar.lod.far',
50
+ mid: 'board.toolbar.lod.mid',
51
+ close: 'board.toolbar.lod.close',
52
+ steps: 'board.toolbar.lod.steps',
53
+ subtasks: 'board.toolbar.lod.subtasks',
54
+ } as const
55
+ const lodLabel = computed(() => t(LOD_LABEL_KEYS[ui.lod]))
51
56
 
52
57
  // Live spend indicator: shown once any tokens have been metered this period.
53
58
  const spend = computed(() => workspace.spend)
@@ -55,11 +60,11 @@ const showSpend = computed(() => !!spend.value && spend.value.costSpent > 0)
55
60
  const spendLabel = computed(() => {
56
61
  const s = spend.value
57
62
  if (!s) return ''
58
- const fmt = (n: number) => {
63
+ const fmt = (value: number) => {
59
64
  try {
60
- return new Intl.NumberFormat(undefined, { style: 'currency', currency: s.currency }).format(n)
65
+ return n(value, { key: 'currency', currency: s.currency })
61
66
  } catch {
62
- return `${n.toFixed(2)} ${s.currency}`
67
+ return `${value.toFixed(2)} ${s.currency}`
63
68
  }
64
69
  }
65
70
  return `${fmt(s.costSpent)} / ${fmt(s.costLimit)}`
@@ -70,7 +75,7 @@ const decisionItems = computed(() =>
70
75
  execution.openDecisions.map((d) => {
71
76
  const b = board.getBlock(d.blockId)
72
77
  return {
73
- label: b?.title ?? 'Block',
78
+ label: b?.title ?? t('common.block'),
74
79
  description: d.decision.question,
75
80
  icon: 'i-lucide-circle-help',
76
81
  onSelect: () => ui.openDecision(d.instanceId, d.decision.id),
@@ -127,7 +132,7 @@ const decisionItems = computed(() =>
127
132
  >
128
133
  {{ execution.pendingDecisionCount
129
134
  }}<span class="hidden sm:inline"
130
- >&nbsp;{{ $t('board.toolbar.decisionWord', execution.pendingDecisionCount) }}</span
135
+ >&nbsp;{{ t('board.toolbar.decisionWord', execution.pendingDecisionCount) }}</span
131
136
  >
132
137
  </UButton>
133
138
  </UDropdownMenu>
@@ -135,7 +140,7 @@ const decisionItems = computed(() =>
135
140
  <!-- in-org sharing: add an existing org service to this board -->
136
141
  <UDropdownMenu v-if="mountableItems.length" :items="mountableItems">
137
142
  <UButton color="neutral" variant="ghost" size="sm" icon="i-lucide-plus-circle">
138
- <span class="hidden sm:inline">{{ $t('board.toolbar.addService') }}</span>
143
+ <span class="hidden sm:inline">{{ t('board.toolbar.addService') }}</span>
139
144
  </UButton>
140
145
  </UDropdownMenu>
141
146
 
@@ -149,7 +154,9 @@ const decisionItems = computed(() =>
149
154
  variant="soft"
150
155
  size="sm"
151
156
  icon="i-lucide-wallet"
152
- :title="spend?.exceeded ? 'Spend limit reached — runs paused' : 'Token spend this month'"
157
+ :title="
158
+ spend?.exceeded ? t('board.toolbar.spendLimitReached') : t('board.toolbar.spendTitle')
159
+ "
153
160
  >
154
161
  <span class="hidden sm:inline">{{ spendLabel }}</span>
155
162
  </UButton>
@@ -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