@cat-factory/app 0.47.7 → 0.47.9

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.
@@ -7,6 +7,7 @@
7
7
  // the user returns to the board, summoned back (a notification) only if the re-run raises new
8
8
  // options or hits the iteration cap. The converged direction — not the original description — is
9
9
  // what the downstream stage (the requirements review / the architect) consumes.
10
+ import IterationCapPrompt from '~/components/pipeline/IterationCapPrompt.vue'
10
11
  import { parseOutputOutline } from '~/utils/agentOutput'
11
12
  import type {
12
13
  BrainstormItem,
@@ -8,6 +8,7 @@
8
8
  // findings or hits the iteration cap. The clarified bug report — not the original
9
9
  // description — is what every downstream agent step (the bug investigator, the coder)
10
10
  // consumes.
11
+ import IterationCapPrompt from '~/components/pipeline/IterationCapPrompt.vue'
11
12
  import { parseOutputOutline } from '~/utils/agentOutput'
12
13
  import type {
13
14
  ClarityItemStatus,
@@ -7,12 +7,13 @@
7
7
  // gates; it branches on the step's `agentKind` for the copy and the failure detail.
8
8
  import { computed, ref } from 'vue'
9
9
  import { agentKindMeta } from '~/utils/catalog'
10
- import type { GateStepState } from '~/types/execution'
10
+ import type { GateAttempt, GateStepState } from '~/types/execution'
11
11
  import StepRestartControl from '~/components/panels/StepRestartControl.vue'
12
12
  import StepRunMeta from '~/components/panels/StepRunMeta.vue'
13
13
 
14
14
  const board = useBoardStore()
15
15
  const execution = useExecutionStore()
16
+ const { t, d } = useI18n()
16
17
 
17
18
  // Synchronous window: it reads its state straight off the execution step, so there's
18
19
  // nothing to fetch on open (no `onOpen` loader).
@@ -39,10 +40,10 @@ const helperMeta = computed(() => agentKindMeta(helperKind.value))
39
40
 
40
41
  const subtitle = computed(() =>
41
42
  isHumanReview.value
42
- ? 'Waits for a human code review on the PR, looping the fixer on comments'
43
+ ? t('gates.subtitle.humanReview')
43
44
  : isCi.value
44
- ? 'Gates the PR on green CI, looping the CI fixer on failure'
45
- : 'Gates the PR on a clean merge, looping the resolver on conflicts',
45
+ ? t('gates.subtitle.ci')
46
+ : t('gates.subtitle.conflicts'),
46
47
  )
47
48
 
48
49
  // Human-review: approval progress + the freeform "request a fix" control.
@@ -67,8 +68,15 @@ const shortSha = computed(() => (gate.value?.headSha ? gate.value.headSha.slice(
67
68
  // The helper-agent attempts this gate dispatched, newest first for the timeline.
68
69
  const attempts = computed(() => [...(gate.value?.attemptLog ?? [])].reverse())
69
70
 
71
+ // Exhaustive map of the attempt outcome enum → label (literal keys keep the typed-key
72
+ // drift guard live, vs a runtime-built `gates.outcome.${outcome}`).
73
+ const OUTCOME_LABELS = computed<Record<GateAttempt['outcome'], string>>(() => ({
74
+ completed: t('gates.outcome.completed'),
75
+ failed: t('gates.outcome.failed'),
76
+ }))
77
+
70
78
  function formatClock(ms?: number | null): string | null {
71
- return ms ? new Date(ms).toLocaleString() : null
79
+ return ms ? d(new Date(ms), 'long') : null
72
80
  }
73
81
 
74
82
  /**
@@ -94,45 +102,62 @@ const status = computed<GateDisplayStatus>(() => {
94
102
  return 'checking'
95
103
  })
96
104
 
97
- const STATUS_META: Record<
98
- GateDisplayStatus,
99
- { label: string; badge: 'success' | 'warning' | 'error' | 'neutral'; icon: string; text: string }
100
- > = {
105
+ const STATUS_META = computed<
106
+ Record<
107
+ GateDisplayStatus,
108
+ {
109
+ label: string
110
+ badge: 'success' | 'warning' | 'error' | 'neutral'
111
+ icon: string
112
+ text: string
113
+ }
114
+ >
115
+ >(() => ({
101
116
  passed: {
102
- label: 'Passed',
117
+ label: t('gates.status.passed'),
103
118
  badge: 'success',
104
119
  icon: 'i-lucide-circle-check',
105
120
  text: 'text-emerald-300',
106
121
  },
107
- 'gave-up': { label: 'Gave up', badge: 'error', icon: 'i-lucide-circle-x', text: 'text-rose-300' },
108
- fixing: { label: 'Fixing', badge: 'warning', icon: 'i-lucide-loader', text: 'text-amber-300' },
122
+ 'gave-up': {
123
+ label: t('gates.status.gaveUp'),
124
+ badge: 'error',
125
+ icon: 'i-lucide-circle-x',
126
+ text: 'text-rose-300',
127
+ },
128
+ fixing: {
129
+ label: t('gates.status.fixing'),
130
+ badge: 'warning',
131
+ icon: 'i-lucide-loader',
132
+ text: 'text-amber-300',
133
+ },
109
134
  failing: {
110
- label: 'Failing',
135
+ label: t('gates.status.failing'),
111
136
  badge: 'error',
112
137
  icon: 'i-lucide-circle-x',
113
138
  text: 'text-rose-300',
114
139
  },
115
140
  pending: {
116
- label: 'Pending',
141
+ label: t('gates.status.pending'),
117
142
  badge: 'neutral',
118
143
  icon: 'i-lucide-clock',
119
144
  text: 'text-slate-300',
120
145
  },
121
146
  checking: {
122
- label: 'Checking',
147
+ label: t('gates.status.checking'),
123
148
  badge: 'neutral',
124
149
  icon: 'i-lucide-loader',
125
150
  text: 'text-slate-300',
126
151
  },
127
- }
152
+ }))
128
153
 
129
154
  // The conflicts gate has no structured detail (GitHub reports mergeability as a single
130
155
  // verdict, no file list), so the window shows the verdict + a note rather than a list.
131
156
  const conflictVerdict = computed(() => {
132
- if (status.value === 'passed') return 'Mergeable'
133
- if (gate.value?.lastVerdict === 'pending') return 'Computing mergeability…'
134
- if (gate.value?.lastVerdict === 'fail') return 'Conflicts with base'
135
- return 'Unknown'
157
+ if (status.value === 'passed') return t('gates.conflict.mergeable')
158
+ if (gate.value?.lastVerdict === 'pending') return t('gates.conflict.computing')
159
+ if (gate.value?.lastVerdict === 'fail') return t('gates.conflict.conflicts')
160
+ return t('gates.conflict.unknown')
136
161
  })
137
162
  </script>
138
163
 
@@ -183,10 +208,9 @@ const conflictVerdict = computed(() => {
183
208
  class="flex h-full flex-col items-center justify-center gap-2 text-center text-slate-400"
184
209
  >
185
210
  <UIcon :name="meta.icon" class="h-8 w-8 opacity-40" />
186
- <p class="text-sm">No gate activity yet.</p>
211
+ <p class="text-sm">{{ t('gates.noActivity') }}</p>
187
212
  <p class="max-w-sm text-[11px] text-slate-500">
188
- The precheck runs once the PR is open. While it polls, the step shows live state on
189
- the board.
213
+ {{ t('gates.noActivityHint') }}
190
214
  </p>
191
215
  </div>
192
216
 
@@ -201,9 +225,7 @@ const conflictVerdict = computed(() => {
201
225
  class="mt-0.5 h-4 w-4 shrink-0 text-emerald-400"
202
226
  />
203
227
  <p class="text-[13px] leading-relaxed text-emerald-200">
204
- {{
205
- step?.output || (isCi ? 'CI is green.' : 'The PR merges cleanly with its base.')
206
- }}
228
+ {{ step?.output || (isCi ? t('gates.passedCi') : t('gates.passedConflicts')) }}
207
229
  </p>
208
230
  </div>
209
231
 
@@ -214,14 +236,20 @@ const conflictVerdict = computed(() => {
214
236
  >
215
237
  <UIcon name="i-lucide-users" class="h-4 w-4 shrink-0 text-violet-300" />
216
238
  <span class="text-[13px] text-slate-200">
217
- {{ gate.lastApprovals ?? 0 }} / {{ requiredApprovals }} approval{{
218
- requiredApprovals === 1 ? '' : 's'
239
+ {{
240
+ t(
241
+ 'gates.humanReview.approvals',
242
+ { approved: gate.lastApprovals ?? 0, required: requiredApprovals },
243
+ requiredApprovals,
244
+ )
219
245
  }}
220
- <template v-if="status === 'fixing'"> · fixer addressing comments…</template>
246
+ <template v-if="status === 'fixing'">
247
+ {{ t('gates.humanReview.suffixFixing') }}</template
248
+ >
221
249
  <template v-else-if="status === 'failing'">
222
- · review comments to address</template
250
+ {{ t('gates.humanReview.suffixFailing') }}</template
223
251
  >
224
- <template v-else> · awaiting review</template>
252
+ <template v-else> {{ t('gates.humanReview.suffixAwaiting') }}</template>
225
253
  </span>
226
254
  </div>
227
255
  <p
@@ -237,7 +265,7 @@ const conflictVerdict = computed(() => {
237
265
  rel="noopener"
238
266
  class="mt-2 inline-flex items-center gap-1 text-[12px] text-sky-300 hover:text-sky-200 hover:underline"
239
267
  >
240
- Review pull request on GitHub
268
+ {{ t('gates.humanReview.reviewPr') }}
241
269
  <UIcon name="i-lucide-external-link" class="h-3 w-3" />
242
270
  </a>
243
271
 
@@ -246,17 +274,16 @@ const conflictVerdict = computed(() => {
246
274
  <h3
247
275
  class="mb-1.5 text-[11px] font-semibold uppercase tracking-wide text-slate-500"
248
276
  >
249
- Request a fix
277
+ {{ t('gates.humanReview.requestFixHeading') }}
250
278
  </h3>
251
279
  <p class="mb-2 text-[11px] leading-relaxed text-slate-500">
252
- Describe a change for the fixer to make on the PR branch now (in addition to any
253
- review comments, which it addresses automatically).
280
+ {{ t('gates.humanReview.requestFixDescription') }}
254
281
  </p>
255
282
  <textarea
256
283
  v-model="fixInstructions"
257
284
  rows="3"
258
285
  :disabled="fixBusy"
259
- placeholder="e.g. rename the helper and add a unit test for the empty-input case"
286
+ :placeholder="t('gates.humanReview.requestFixPlaceholder')"
260
287
  class="w-full resize-y rounded-md border border-slate-800 bg-slate-950/60 px-3 py-2 text-[13px] text-slate-200 placeholder:text-slate-600 focus:border-violet-500/60 focus:outline-none"
261
288
  />
262
289
  <div class="mt-2 flex justify-end">
@@ -268,7 +295,7 @@ const conflictVerdict = computed(() => {
268
295
  :disabled="fixBusy || fixInstructions.trim().length === 0"
269
296
  @click="submitFix"
270
297
  >
271
- Request fix
298
+ {{ t('gates.humanReview.requestFix') }}
272
299
  </UButton>
273
300
  </div>
274
301
  </section>
@@ -277,7 +304,7 @@ const conflictVerdict = computed(() => {
277
304
  <!-- CI: failing checks -->
278
305
  <template v-else-if="isCi">
279
306
  <h3 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
280
- Failing checks
307
+ {{ t('gates.ci.failingChecks') }}
281
308
  </h3>
282
309
  <ul v-if="failingChecks.length" class="space-y-1">
283
310
  <li
@@ -292,7 +319,7 @@ const conflictVerdict = computed(() => {
292
319
  target="_blank"
293
320
  rel="noopener"
294
321
  class="group min-w-0 flex-1 truncate text-[13px] text-sky-300 hover:text-sky-200 hover:underline"
295
- :title="`Open ${c.name} on GitHub`"
322
+ :title="t('gates.ci.openOnGithub', { name: c.name })"
296
323
  >
297
324
  {{ c.name }}
298
325
  <UIcon
@@ -304,19 +331,19 @@ const conflictVerdict = computed(() => {
304
331
  c.name
305
332
  }}</span>
306
333
  <span class="shrink-0 text-[11px] uppercase text-rose-300">
307
- {{ c.conclusion ?? 'failure' }}
334
+ {{ c.conclusion ?? t('gates.ci.conclusionFallback') }}
308
335
  </span>
309
336
  </li>
310
337
  </ul>
311
338
  <p v-else class="text-[13px] leading-relaxed text-slate-300">
312
- {{ gate.lastFailureSummary || 'CI has not reported a failure on this commit.' }}
339
+ {{ gate.lastFailureSummary || t('gates.ci.failureFallback') }}
313
340
  </p>
314
341
  </template>
315
342
 
316
343
  <!-- Conflicts: verdict + the resolver's account of what it left -->
317
344
  <template v-else>
318
345
  <h3 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
319
- Mergeability
346
+ {{ t('gates.conflicts.mergeability') }}
320
347
  </h3>
321
348
  <div
322
349
  class="flex items-center gap-2 rounded-md border border-slate-800 bg-slate-950/40 px-3 py-2"
@@ -344,7 +371,7 @@ const conflictVerdict = computed(() => {
344
371
  rel="noopener"
345
372
  class="mt-2 inline-flex items-center gap-1 text-[12px] text-sky-300 hover:text-sky-200 hover:underline"
346
373
  >
347
- View pull request on GitHub
374
+ {{ t('gates.conflicts.viewPr') }}
348
375
  <UIcon name="i-lucide-external-link" class="h-3 w-3" />
349
376
  </a>
350
377
  </template>
@@ -352,7 +379,7 @@ const conflictVerdict = computed(() => {
352
379
  <!-- Attempt history (both gates): what each helper run did and how it ended. -->
353
380
  <section v-if="attempts.length" class="mt-5">
354
381
  <h3 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
355
- {{ helperMeta.label }} attempts
382
+ {{ t('gates.attemptsHeading', { helper: helperMeta.label }) }}
356
383
  </h3>
357
384
  <ol class="space-y-2">
358
385
  <li
@@ -361,14 +388,14 @@ const conflictVerdict = computed(() => {
361
388
  class="rounded-md border border-slate-800 bg-slate-950/40 px-3 py-2"
362
389
  >
363
390
  <div class="flex items-center gap-2">
364
- <span class="text-[12px] font-semibold text-slate-200"
365
- >Attempt {{ a.attempt }}</span
366
- >
391
+ <span class="text-[12px] font-semibold text-slate-200">{{
392
+ t('gates.attempt', { number: a.attempt })
393
+ }}</span>
367
394
  <UBadge
368
395
  :color="a.outcome === 'failed' ? 'error' : 'neutral'"
369
396
  variant="subtle"
370
397
  size="sm"
371
- >{{ a.outcome }}</UBadge
398
+ >{{ OUTCOME_LABELS[a.outcome] }}</UBadge
372
399
  >
373
400
  <span v-if="formatClock(a.at)" class="ml-auto text-[11px] text-slate-500">{{
374
401
  formatClock(a.at)
@@ -392,7 +419,7 @@ const conflictVerdict = computed(() => {
392
419
  >
393
420
  <div v-if="gate">
394
421
  <h4 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
395
- State
422
+ {{ t('gates.sidebar.state') }}
396
423
  </h4>
397
424
  <div class="flex items-center gap-2 text-[13px]">
398
425
  <UIcon
@@ -412,21 +439,29 @@ const conflictVerdict = computed(() => {
412
439
  <!-- The human-review gate's budget is effectively unbounded (it waits for a human
413
440
  indefinitely), so render a plain round count rather than "0/9007199254740991". -->
414
441
  <template v-if="isHumanReview">
415
- {{ gate.attempts }} fix round{{ gate.attempts === 1 ? '' : 's' }}
442
+ {{ t('gates.sidebar.fixRounds', { count: gate.attempts }, gate.attempts) }}
416
443
  </template>
417
444
  <template v-else>
418
- {{ gate.attempts }}/{{ gate.maxAttempts }} attempt{{
419
- gate.maxAttempts === 1 ? '' : 's'
445
+ {{
446
+ t(
447
+ 'gates.sidebar.attempts',
448
+ { attempts: gate.attempts, max: gate.maxAttempts },
449
+ gate.maxAttempts,
450
+ )
420
451
  }}
421
452
  </template>
422
- <template v-if="gate.phase === 'working'"> · running…</template>
423
- <template v-else-if="gate.attempts === 0"> · not needed yet</template>
453
+ <template v-if="gate.phase === 'working'">
454
+ {{ t('gates.sidebar.suffixRunning') }}</template
455
+ >
456
+ <template v-else-if="gate.attempts === 0">
457
+ {{ t('gates.sidebar.suffixNotNeeded') }}</template
458
+ >
424
459
  </p>
425
460
  </div>
426
461
 
427
462
  <div v-if="shortSha">
428
463
  <h4 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
429
- Gated commit
464
+ {{ t('gates.sidebar.gatedCommit') }}
430
465
  </h4>
431
466
  <p class="font-mono text-[12px] text-slate-300">{{ shortSha }}</p>
432
467
  </div>
@@ -444,8 +479,7 @@ const conflictVerdict = computed(() => {
444
479
  />
445
480
 
446
481
  <p class="mt-auto text-[10px] leading-relaxed text-slate-600">
447
- A gate runs a programmatic precheck and only spins up the
448
- {{ helperMeta.label }} when it fails — a green check advances with nothing spun up.
482
+ {{ t('gates.sidebar.footer', { helper: helperMeta.label }) }}
449
483
  </p>
450
484
  </aside>
451
485
  </div>
@@ -4,6 +4,7 @@ import { useLocalStorage } from '@vueuse/core'
4
4
  import type { AgentKind } from '~/types/domain'
5
5
  import { AGENT_CATEGORIES, OBSERVABILITY_GATE_ARCHETYPE } from '~/utils/catalog'
6
6
 
7
+ const { t } = useI18n()
7
8
  const agents = useAgentsStore()
8
9
  const releaseHealth = useReleaseHealthStore()
9
10
  defineEmits<{ (e: 'add', kind: AgentKind): void }>()
@@ -25,7 +26,8 @@ const groups = computed(() => {
25
26
  agents: palette.value.filter((a) => a.category === cat.id),
26
27
  }))
27
28
  const custom = palette.value.filter((a) => !a.category)
28
- if (custom.length) ordered.push({ id: 'custom', label: 'Custom agents', agents: custom })
29
+ if (custom.length)
30
+ ordered.push({ id: 'custom', label: t('palette.customAgents'), agents: custom })
29
31
  return ordered.filter((g) => g.agents.length)
30
32
  })
31
33
 
@@ -43,7 +45,7 @@ function toggle(id: string) {
43
45
 
44
46
  <template>
45
47
  <div class="space-y-2">
46
- <p class="px-1 text-[11px] text-slate-500">Click an agent to append it to the pipeline.</p>
48
+ <p class="px-1 text-[11px] text-slate-500">{{ t('palette.hint') }}</p>
47
49
  <div class="space-y-2">
48
50
  <section v-for="g in groups" :key="g.id">
49
51
  <button
@@ -8,6 +8,7 @@ import StepMetadataCard from '~/components/panels/StepMetadataCard.vue'
8
8
  import StepTestReport from '~/components/panels/StepTestReport.vue'
9
9
  import EnvironmentStatusPanel from '~/components/environments/EnvironmentStatusPanel.vue'
10
10
  import ProvisioningLogsDrawer from '~/components/provisioning/ProvisioningLogsDrawer.vue'
11
+ import IterationCapPrompt from '~/components/pipeline/IterationCapPrompt.vue'
11
12
  import { useStepTimer } from '~/composables/useStepTimer'
12
13
  import { useStepProse } from '~/composables/useStepProse'
13
14
  import { useStepApproval } from '~/composables/useStepApproval'
@@ -12,6 +12,7 @@ import TaskDependencies from '~/components/panels/inspector/TaskDependencies.vue
12
12
  import TaskStructure from '~/components/panels/inspector/TaskStructure.vue'
13
13
  import TaskRunSettings from '~/components/panels/inspector/TaskRunSettings.vue'
14
14
  import TaskExecution from '~/components/panels/inspector/TaskExecution.vue'
15
+ import TaskEstimateBadge from '~/components/panels/inspector/TaskEstimateBadge.vue'
15
16
  import EpicChildren from '~/components/panels/inspector/EpicChildren.vue'
16
17
  import RecurringScheduleSettings from '~/components/panels/inspector/RecurringScheduleSettings.vue'
17
18
  import AgentFailureCard from '~/components/board/AgentFailureCard.vue'
@@ -7,7 +7,7 @@ import type { IterationCapChoice } from '~/types/execution'
7
7
  // or stop and reset the task. Used by both the requirements-review window and the
8
8
  // companion step detail (Spec Reviewer / Reviewer / Architect Companion), so the two
9
9
  // gates present an identical choice rather than each rolling its own.
10
- withDefaults(
10
+ const props = withDefaults(
11
11
  defineProps<{
12
12
  heading: string
13
13
  detail: string
@@ -16,14 +16,17 @@ withDefaults(
16
16
  proceedLabel?: string
17
17
  stopLabel?: string
18
18
  }>(),
19
- {
20
- loading: false,
21
- extraRoundLabel: 'One more round',
22
- proceedLabel: 'Proceed anyway',
23
- stopLabel: 'Stop & reset task',
24
- },
19
+ { loading: false },
25
20
  )
26
21
 
22
+ const { t } = useI18n()
23
+
24
+ // The three button labels default to the shared i18n copy, but a caller may override
25
+ // any of them with surface-specific wording.
26
+ const extraRound = computed(() => props.extraRoundLabel ?? t('pipeline.iterationCap.extraRound'))
27
+ const proceed = computed(() => props.proceedLabel ?? t('pipeline.iterationCap.proceed'))
28
+ const stop = computed(() => props.stopLabel ?? t('pipeline.iterationCap.stopReset'))
29
+
27
30
  const emit = defineEmits<{ resolve: [choice: IterationCapChoice] }>()
28
31
  </script>
29
32
 
@@ -43,7 +46,7 @@ const emit = defineEmits<{ resolve: [choice: IterationCapChoice] }>()
43
46
  :loading="loading"
44
47
  @click="emit('resolve', 'extra-round')"
45
48
  >
46
- {{ extraRoundLabel }}
49
+ {{ extraRound }}
47
50
  </UButton>
48
51
  <UButton
49
52
  color="warning"
@@ -53,7 +56,7 @@ const emit = defineEmits<{ resolve: [choice: IterationCapChoice] }>()
53
56
  :loading="loading"
54
57
  @click="emit('resolve', 'proceed')"
55
58
  >
56
- {{ proceedLabel }}
59
+ {{ proceed }}
57
60
  </UButton>
58
61
  <UButton
59
62
  color="error"
@@ -63,7 +66,7 @@ const emit = defineEmits<{ resolve: [choice: IterationCapChoice] }>()
63
66
  :loading="loading"
64
67
  @click="emit('resolve', 'stop-reset')"
65
68
  >
66
- {{ stopLabel }}
69
+ {{ stop }}
67
70
  </UButton>
68
71
  </div>
69
72
  </div>