@cat-factory/app 0.50.1 → 0.51.0

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.
@@ -0,0 +1,116 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import type { PipelineStep, RunContainerStatus } from '~/types/execution'
4
+ import { containerPhaseLabel } from '~/utils/pipelineRender'
5
+
6
+ // The per-run container lifecycle for a container-backed step: its status (spinning up /
7
+ // running / errored / reclaimed), the live phase (preparing the checkout vs the agent
8
+ // making calls), and the container's id + reachable URL once up. Shared by the generic
9
+ // step detail (StepMetadataCard) and the dedicated Tester window so both surface WHAT the
10
+ // container is doing and WHERE it lives instead of a bare "working" — identical parity.
11
+ const props = defineProps<{ step: PipelineStep; runFailed: boolean }>()
12
+
13
+ const { t, te } = useI18n()
14
+
15
+ // The container lifecycle to display. The backend persists starting / up / errored; we
16
+ // derive `destroyed` once the container is reclaimed — when this step has finished or the
17
+ // whole run is no longer running (the per-run container goes as a unit). `errored` wins,
18
+ // and a reclaimed container ALWAYS reads "destroyed" — even one caught mid cold-boot
19
+ // (`starting`) when the run terminated must NOT linger as a perpetual spinner.
20
+ const containerStatus = computed<RunContainerStatus | null>(() => {
21
+ const c = props.step.container
22
+ if (!c) return null
23
+ if (c.status === 'errored') return 'errored'
24
+ if (props.step.state === 'done' || props.runFailed) return 'destroyed'
25
+ return c.status
26
+ })
27
+
28
+ // Static literal keys (not a runtime-built `t(`…${status}`)`) so the typed-message-keys
29
+ // check covers them; exhaustive over the union so a new status fails the typecheck here.
30
+ const CONTAINER_STATUS_KEYS: Record<RunContainerStatus, string> = {
31
+ starting: 'panels.stepMeta.container.status.starting',
32
+ up: 'panels.stepMeta.container.status.up',
33
+ errored: 'panels.stepMeta.container.status.errored',
34
+ destroyed: 'panels.stepMeta.container.status.destroyed',
35
+ }
36
+ const CONTAINER_STATUS_META: Record<
37
+ RunContainerStatus,
38
+ { icon: string; spin: boolean; cls: string }
39
+ > = {
40
+ starting: {
41
+ icon: 'i-lucide-loader-circle',
42
+ spin: true,
43
+ cls: 'border-sky-900/50 bg-sky-950/30 text-sky-300',
44
+ },
45
+ up: {
46
+ icon: 'i-lucide-box',
47
+ spin: false,
48
+ cls: 'border-emerald-900/50 bg-emerald-950/30 text-emerald-300',
49
+ },
50
+ errored: {
51
+ icon: 'i-lucide-circle-x',
52
+ spin: false,
53
+ cls: 'border-rose-900/50 bg-rose-950/30 text-rose-300',
54
+ },
55
+ destroyed: {
56
+ icon: 'i-lucide-power-off',
57
+ spin: false,
58
+ cls: 'border-slate-800 bg-slate-900/40 text-slate-400',
59
+ },
60
+ }
61
+
62
+ // The friendly phase label (clone → "Preparing workspace", …); only meaningful while up.
63
+ const phaseLabel = computed(() => containerPhaseLabel(props.step.container?.phase, { t, te }))
64
+ </script>
65
+
66
+ <template>
67
+ <!-- Single conditional root so a passed-through `class` (e.g. layout margin) applies
68
+ cleanly. Renders nothing for a non-container step / one not yet dispatched. -->
69
+ <div v-if="containerStatus" data-testid="step-container-status">
70
+ <!-- container lifecycle: status (spinning up / running / errored / reclaimed), the
71
+ live phase (preparing the checkout vs the agent making calls), and the
72
+ container's id + reachable URL once up. -->
73
+ <div
74
+ class="rounded-lg border px-3 py-2 text-[12px]"
75
+ :class="CONTAINER_STATUS_META[containerStatus].cls"
76
+ >
77
+ <div class="flex items-center gap-2">
78
+ <UIcon
79
+ :name="CONTAINER_STATUS_META[containerStatus].icon"
80
+ class="h-4 w-4 shrink-0"
81
+ :class="CONTAINER_STATUS_META[containerStatus].spin ? 'animate-spin' : ''"
82
+ />
83
+ <span class="font-medium">{{ t(CONTAINER_STATUS_KEYS[containerStatus]) }}</span>
84
+ <template v-if="phaseLabel && containerStatus === 'up'">
85
+ <span class="text-slate-500">·</span>
86
+ <span>{{ phaseLabel }}</span>
87
+ </template>
88
+ </div>
89
+ <dl v-if="step.container?.id || step.container?.url" class="mt-2 space-y-1">
90
+ <div v-if="step.container?.id" class="flex items-center gap-2">
91
+ <dt class="shrink-0 text-[11px] uppercase tracking-wide text-slate-500">
92
+ {{ t('panels.stepMeta.container.id') }}
93
+ </dt>
94
+ <dd class="truncate font-mono text-[11px] text-slate-300" :title="step.container.id">
95
+ {{ step.container.id }}
96
+ </dd>
97
+ </div>
98
+ <div v-if="step.container?.url" class="flex items-center gap-2">
99
+ <dt class="shrink-0 text-[11px] uppercase tracking-wide text-slate-500">
100
+ {{ t('panels.stepMeta.container.url') }}
101
+ </dt>
102
+ <dd class="truncate font-mono text-[11px] text-slate-300">
103
+ <a
104
+ :href="step.container.url"
105
+ target="_blank"
106
+ rel="noopener noreferrer"
107
+ class="hover:underline"
108
+ >
109
+ {{ step.container.url }}
110
+ </a>
111
+ </dd>
112
+ </div>
113
+ </dl>
114
+ </div>
115
+ </div>
116
+ </template>
@@ -3,6 +3,7 @@ import { computed } from 'vue'
3
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
+ import StepContainerStatus from '~/components/panels/StepContainerStatus.vue'
6
7
 
7
8
  // The step's metadata card body: state/timing/model/run id, the container cold-boot
8
9
  // phase, the live subtask breakdown, the LLM observability rollup, the applied
@@ -153,15 +154,9 @@ async function copyRunId() {
153
154
  </div>
154
155
  </dl>
155
156
 
156
- <!-- container cold-boot phase: shown until the container is up and
157
- the agent starts reporting progress -->
158
- <div
159
- v-if="step.startingContainer && !runFailed"
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"
161
- >
162
- <UIcon name="i-lucide-loader-circle" class="h-4 w-4 shrink-0 animate-spin" />
163
- <span>{{ t('panels.stepMeta.spinningUpContainer') }}</span>
164
- </div>
157
+ <!-- container lifecycle (status / live phase / id + url) shared with the Tester
158
+ window so both surface what the container is doing and where it lives. -->
159
+ <StepContainerStatus :step="step" :run-failed="runFailed" class="mt-4" />
165
160
 
166
161
  <!-- live subtask breakdown -->
167
162
  <div v-if="step.subtasks && step.subtasks.total > 0" class="mt-4">
@@ -1,7 +1,12 @@
1
1
  <script setup lang="ts">
2
2
  import type { Block } from '~/types/domain'
3
3
  import { agentKindMeta } from '~/utils/catalog'
4
- import { gateCompanionFor, COMPANION_STATE_META, isCompanionKind } from '~/utils/pipelineRender'
4
+ import {
5
+ gateCompanionFor,
6
+ COMPANION_STATE_META,
7
+ isCompanionKind,
8
+ containerPhaseLabel,
9
+ } from '~/utils/pipelineRender'
5
10
  import AgentFailureCard from '~/components/board/AgentFailureCard.vue'
6
11
 
7
12
  const props = defineProps<{ block: Block }>()
@@ -11,7 +16,7 @@ const agentRuns = useAgentRunsStore()
11
16
  const ui = useUiStore()
12
17
  const models = useModelsStore()
13
18
  const reviews = useReviewStage()
14
- const { t } = useI18n()
19
+ const { t, te } = useI18n()
15
20
 
16
21
  // The async stage this task's iterative reviewer gate (requirements-review / clarity-review)
17
22
  // is mid-cycle in (folding the answers, then re-reviewing), or null. While set, the gate is
@@ -68,7 +73,7 @@ function labelForStep(s: {
68
73
  agentKind?: string
69
74
  approval?: { status: string } | null
70
75
  companion?: { exceeded?: boolean } | null
71
- startingContainer?: boolean
76
+ container?: { status: string; phase?: string | null } | null
72
77
  }) {
73
78
  // A step left mid-flight on a failed run reads "Failed", not the misleading "Working".
74
79
  if (stepFailed(s)) return t('inspector.execution.failed')
@@ -79,9 +84,17 @@ function labelForStep(s: {
79
84
  if (s.approval?.status === 'pending' && s.companion?.exceeded)
80
85
  return t('inspector.execution.needsDecision')
81
86
  if (s.approval?.status === 'pending') return t('inspector.execution.needsApproval')
82
- // A container-backed step whose container is still cold-booting (only while the
83
- // run is live a failed run's mid-flight step is no longer spinning up).
84
- if (s.startingContainer && !runFailed.value) return t('inspector.execution.spinningUp')
87
+ // A container-backed step: surface its live lifecycle while the run is running. The
88
+ // container is still cold-booting "Spinning up"; up with a known phase the phase
89
+ // label ("Agent running" / "Preparing workspace"), so a finished cold-boot no longer
90
+ // collapses into a blank "Working". A failed run's mid-flight step isn't booting.
91
+ if (!runFailed.value) {
92
+ if (s.container?.status === 'starting') return t('inspector.execution.spinningUp')
93
+ if (s.container?.status === 'up') {
94
+ const label = containerPhaseLabel(s.container.phase, { t, te })
95
+ if (label) return label
96
+ }
97
+ }
85
98
  const key = stepLabel[s.state]
86
99
  return key ? t(key) : s.state
87
100
  }
@@ -9,6 +9,7 @@ import {
9
9
  isCompanionKind,
10
10
  isFailedStep,
11
11
  FAILED_STEP_META,
12
+ containerPhaseLabel,
12
13
  } from '~/utils/pipelineRender'
13
14
  import StepMetricsBar from '~/components/observability/StepMetricsBar.vue'
14
15
 
@@ -22,7 +23,15 @@ const models = useModelsStore()
22
23
  const ui = useUiStore()
23
24
  const execution = useExecutionStore()
24
25
  const reviews = useReviewStage()
25
- const { t } = useI18n()
26
+ const { t, te } = useI18n()
27
+
28
+ // The friendly container phase label for a step whose container is up — null otherwise.
29
+ // Lets the board fill the gap between the cold-boot badge clearing and the first subtask
30
+ // count (the old "blank working"). Shared with the step-detail card + inspector label.
31
+ function stepPhaseLabel(s: { container?: { status: string; phase?: string | null } | null }) {
32
+ if (s.container?.status !== 'up') return null
33
+ return containerPhaseLabel(s.container.phase, { t, te })
34
+ }
26
35
 
27
36
  // While an iterative reviewer gate (requirements-review / clarity-review) folds the
28
37
  // answers / re-reviews in the background it needs NO human, so its parked approval is
@@ -126,9 +135,9 @@ const total = computed(() => steps.value.length)
126
135
  // human can see at a glance whether the fixer ran or was skipped.
127
136
  const companionByStep = computed(() => steps.value.map((s) => gateCompanionFor(s, runFailed.value)))
128
137
 
129
- // A failed run is no longer executing: a step left mid-flight (state still
130
- // `working`, `startingContainer` still set) must stop looking live — no spinner,
131
- // no pulse, no "spinning up container" phase.
138
+ // A failed run is no longer executing: a step left mid-flight (state still `working`,
139
+ // its container caught mid cold-boot) must stop looking live — no spinner, no pulse,
140
+ // no "spinning up container" phase.
132
141
  const runFailed = computed(() => props.instance.status === 'failed')
133
142
  /** A step that is genuinely, currently working (not a stale mid-flight step). */
134
143
  function liveWorking(state: AgentState) {
@@ -356,16 +365,25 @@ const ITEM_ICON: Record<string, string> = {
356
365
  class="mt-3"
357
366
  />
358
367
 
359
- <!-- container cold-boot phase: shown until the container is up and the
360
- agent starts reporting progress -->
368
+ <!-- container cold-boot phase: shown while the container is spinning up. -->
361
369
  <div
362
- v-if="s.startingContainer && !runFailed"
370
+ v-if="s.container?.status === 'starting' && !runFailed"
363
371
  class="mt-2 flex items-center gap-1.5 text-[11px] text-sky-300"
364
372
  >
365
373
  <UIcon name="i-lucide-loader-circle" class="h-3.5 w-3.5 shrink-0 animate-spin" />
366
374
  <span>{{ t('pipeline.progress.spinningUpContainer') }}</span>
367
375
  </div>
368
376
 
377
+ <!-- container is up: show WHAT it's doing (preparing the checkout vs the agent
378
+ making calls) so the step isn't a blank "working" before subtasks appear. -->
379
+ <div
380
+ v-else-if="stepPhaseLabel(s) && !runFailed"
381
+ class="mt-2 flex items-center gap-1.5 text-[11px] text-emerald-300"
382
+ >
383
+ <UIcon name="i-lucide-box" class="h-3.5 w-3.5 shrink-0" />
384
+ <span>{{ stepPhaseLabel(s) }}</span>
385
+ </div>
386
+
369
387
  <!-- live subtask counts from the agent's todo list -->
370
388
  <div v-if="s.subtasks && s.subtasks.total > 0" class="mt-2">
371
389
  <div class="flex items-center justify-between text-[10px] text-slate-400">
@@ -17,10 +17,13 @@ import { useFocusTrap } from '~/composables/useFocusTrap'
17
17
  import ArtifactLightbox from '~/components/media/ArtifactLightbox.vue'
18
18
  import StepRestartControl from '~/components/panels/StepRestartControl.vue'
19
19
  import StepRunMeta from '~/components/panels/StepRunMeta.vue'
20
+ import StepContainerStatus from '~/components/panels/StepContainerStatus.vue'
21
+ import EnvironmentStatusPanel from '~/components/environments/EnvironmentStatusPanel.vue'
22
+ import ProvisioningLogsDrawer from '~/components/provisioning/ProvisioningLogsDrawer.vue'
20
23
 
21
24
  const board = useBoardStore()
22
25
  const execution = useExecutionStore()
23
- const { t } = useI18n()
26
+ const { t, d } = useI18n()
24
27
 
25
28
  // Per-window blob cache for the captured screenshots; revoked on unmount.
26
29
  const blobs = useArtifactBlobs()
@@ -40,6 +43,20 @@ const step = computed(() => {
40
43
  })
41
44
  const report = computed<TestReport | null>(() => step.value?.test?.lastReport ?? null)
42
45
  const testState = computed(() => step.value?.test ?? null)
46
+ // The inspectable Fixer history: one entry per fixer round (what it was handed + how it
47
+ // ended), newest first, so the otherwise-opaque fixer sub-jobs have a surface here.
48
+ const fixerAttempts = computed(() => [...(testState.value?.attemptLog ?? [])].reverse())
49
+
50
+ // Infrastructure observability — parity with the Coder's generic step detail, so the
51
+ // Tester window surfaces WHERE its job runs (the container lifecycle: spinning up /
52
+ // running phase / id+url / errored), the ephemeral environment it tests against, and the
53
+ // run's infrastructure attempts + logs (container/runner/env spin-up), not just the
54
+ // report. The container/subtask signals already flow onto the step via the generic poll.
55
+ const runFailed = computed(() => instance.value?.status === 'failed')
56
+ const stepEnvironment = computed(() => step.value?.environment ?? null)
57
+ const executionId = computed(() => instance.value?.id ?? null)
58
+ // The infra-attempts log drawer is opened on demand (it fetches the per-run log rows).
59
+ const showProvisioning = ref(false)
43
60
 
44
61
  const screenshots = computed<TestScreenshot[]>(() => report.value?.screenshots ?? [])
45
62
  // Resolve each capture into an object URL for the gallery + lightbox. The shared cache
@@ -258,6 +275,7 @@ const GROUP_STATUS_META: Record<ScenarioGroup['status'], { icon: string; text: s
258
275
  tabindex="-1"
259
276
  role="dialog"
260
277
  aria-modal="true"
278
+ data-testid="tester-report-window"
261
279
  :aria-label="t('testing.title')"
262
280
  class="m-4 flex w-full max-w-5xl flex-col overflow-hidden rounded-2xl border border-slate-800 bg-slate-900 shadow-2xl focus:outline-none"
263
281
  >
@@ -310,11 +328,118 @@ const GROUP_STATUS_META: Record<ScenarioGroup['status'], { icon: string; text: s
310
328
  </header>
311
329
 
312
330
  <div class="flex min-h-0 flex-1">
313
- <!-- Main: scenarios → outcomes → concerns tree -->
314
- <div class="min-w-0 flex-1 overflow-y-auto px-5 py-4">
331
+ <!-- Main: infrastructure observability + scenarios → outcomes → concerns tree -->
332
+ <div class="min-w-0 flex-1 space-y-4 overflow-y-auto px-5 py-4">
333
+ <!-- Infrastructure: container lifecycle (where + what it's doing), the
334
+ ephemeral environment, and the run's infra attempts + logs — parity with
335
+ the Coder's step detail. Shown even before a report lands, so the infra
336
+ spin-up is visible WHILE the Tester is still standing it up. -->
337
+ <!-- Only when there's genuine infrastructure to show — a container or an ephemeral
338
+ environment. A no-infra tester (no container, no env) has no infra attempts
339
+ either, so we don't render an empty header + a log toggle over nothing. -->
340
+ <section
341
+ v-if="step && (step.container || stepEnvironment)"
342
+ data-testid="tester-infrastructure"
343
+ class="space-y-3"
344
+ >
345
+ <h3 class="text-[11px] font-semibold uppercase tracking-wide text-slate-500">
346
+ {{ t('testing.infrastructure') }}
347
+ </h3>
348
+ <StepContainerStatus :step="step" :run-failed="runFailed" />
349
+ <EnvironmentStatusPanel v-if="stepEnvironment" :environment="stepEnvironment" />
350
+ <div v-if="executionId">
351
+ <UButton
352
+ :icon="showProvisioning ? 'i-lucide-chevron-up' : 'i-lucide-scroll-text'"
353
+ variant="ghost"
354
+ size="xs"
355
+ data-testid="tester-infra-attempts-toggle"
356
+ @click="showProvisioning = !showProvisioning"
357
+ >
358
+ {{
359
+ showProvisioning
360
+ ? t('panels.stepDetail.hideInfraAttempts')
361
+ : t('panels.stepDetail.infraAttempts')
362
+ }}
363
+ </UButton>
364
+ <ProvisioningLogsDrawer
365
+ v-if="showProvisioning"
366
+ class="mt-2"
367
+ :execution-id="executionId"
368
+ />
369
+ </div>
370
+ </section>
371
+
372
+ <!-- Fixer timeline: one inspectable entry per fixer round (what it was handed and
373
+ how it ended), so the otherwise-opaque fixer sub-jobs have a surface — the
374
+ analogue of the polling gate's attempt history. -->
375
+ <section
376
+ v-if="fixerAttempts.length"
377
+ data-testid="tester-fixer-attempts"
378
+ class="space-y-2"
379
+ >
380
+ <h3 class="text-[11px] font-semibold uppercase tracking-wide text-slate-500">
381
+ {{ t('testing.fixerAttempts') }}
382
+ </h3>
383
+ <ol class="space-y-2">
384
+ <li
385
+ v-for="a in fixerAttempts"
386
+ :key="a.attempt"
387
+ data-testid="tester-fixer-attempt"
388
+ class="rounded-lg border border-slate-800 bg-slate-900/60 px-3 py-2"
389
+ >
390
+ <div class="flex items-center gap-2">
391
+ <UIcon
392
+ :name="a.outcome === 'completed' ? 'i-lucide-wrench' : 'i-lucide-circle-x'"
393
+ class="h-3.5 w-3.5 shrink-0"
394
+ :class="a.outcome === 'completed' ? 'text-amber-300' : 'text-rose-400'"
395
+ />
396
+ <span class="text-[13px] font-medium text-slate-200">
397
+ {{ t('testing.fixerTimeline.attempt', { n: a.attempt }) }}
398
+ </span>
399
+ <UBadge
400
+ :color="a.outcome === 'completed' ? 'neutral' : 'error'"
401
+ variant="subtle"
402
+ size="sm"
403
+ >
404
+ {{
405
+ a.outcome === 'completed'
406
+ ? t('testing.fixerTimeline.completed')
407
+ : t('testing.fixerTimeline.failed')
408
+ }}
409
+ </UBadge>
410
+ <span class="ml-auto text-[11px] text-slate-500">{{
411
+ d(new Date(a.at), 'short')
412
+ }}</span>
413
+ </div>
414
+ <p v-if="a.summary" class="mt-1 text-[12px] leading-snug text-slate-400">
415
+ {{ a.summary }}
416
+ </p>
417
+ <div v-if="a.concerns && a.concerns.length" class="mt-1.5">
418
+ <p class="text-[11px] text-slate-500">
419
+ {{ t('testing.fixerTimeline.addressed') }}
420
+ </p>
421
+ <ul class="mt-1 space-y-0.5">
422
+ <li
423
+ v-for="(c, ci) in a.concerns"
424
+ :key="`fa${a.attempt}-c${ci}`"
425
+ class="flex items-center gap-1.5 text-[12px] text-slate-300"
426
+ >
427
+ <span
428
+ class="rounded px-1 text-[10px] uppercase"
429
+ :class="SEVERITY_META[c.severity].chip"
430
+ >{{ SEVERITY_LABELS[c.severity] }}</span
431
+ >
432
+ <span class="truncate">{{ c.title }}</span>
433
+ </li>
434
+ </ul>
435
+ </div>
436
+ </li>
437
+ </ol>
438
+ </section>
439
+
315
440
  <div
316
441
  v-if="!report"
317
- class="flex h-full flex-col items-center justify-center gap-2 text-center text-slate-400"
442
+ class="flex flex-col items-center justify-center gap-2 py-12 text-center text-slate-400"
318
443
  >
319
444
  <UIcon name="i-lucide-flask-conical" class="h-8 w-8 opacity-40" />
320
445
  <p class="text-sm">{{ t('testing.empty.title') }}</p>
@@ -35,6 +35,8 @@ export type {
35
35
  TesterStepState,
36
36
  HumanTestEnvironment,
37
37
  RunEnvironment,
38
+ RunContainer,
39
+ RunContainerStatus,
38
40
  HumanTestRound,
39
41
  HumanTestStepState,
40
42
  VisualConfirmStepState,
@@ -133,6 +133,24 @@ export function isCompanionKind(kind: string): boolean {
133
133
  )
134
134
  }
135
135
 
136
+ /**
137
+ * The friendly label for a container's live phase (clone → "Preparing workspace",
138
+ * agent → "Agent running", …), falling back to the raw phase string for an unknown/new
139
+ * phase (the phase vocabulary is harness-controlled and open-ended). `null` when there's
140
+ * no phase to show. Kept here so the three views that render it (the step-detail card,
141
+ * the inspector label, the board node) resolve it identically rather than re-deriving the
142
+ * key + `te()`-fallback inline. `t`/`te` are passed in since this is a pure util, not a
143
+ * composable.
144
+ */
145
+ export function containerPhaseLabel(
146
+ phase: string | null | undefined,
147
+ i18n: { t: (key: string) => string; te: (key: string) => boolean },
148
+ ): string | null {
149
+ if (!phase) return null
150
+ const key = `panels.stepMeta.container.phase.${phase}`
151
+ return i18n.te(key) ? i18n.t(key) : phase
152
+ }
153
+
136
154
  /**
137
155
  * Tailwind classes for a subtask-item status icon. An in-progress item spins only
138
156
  * while the run is live: once the run has failed, a step left mid-flight (its item
@@ -601,6 +601,23 @@
601
601
  "run": "Run",
602
602
  "clickToCopy": "{id} (click to copy)",
603
603
  "spinningUpContainer": "Spinning up container…",
604
+ "container": {
605
+ "status": {
606
+ "starting": "Spinning up container…",
607
+ "up": "Container running",
608
+ "errored": "Container errored",
609
+ "destroyed": "Container reclaimed"
610
+ },
611
+ "phase": {
612
+ "starting": "Starting up",
613
+ "clone": "Preparing workspace",
614
+ "agent": "Agent running",
615
+ "push": "Pushing changes",
616
+ "done": "Finishing up"
617
+ },
618
+ "id": "Container",
619
+ "url": "Address"
620
+ },
604
621
  "subtasks": "Subtasks · {completed}/{total}",
605
622
  "standardsApplied": "Standards applied",
606
623
  "decision": "Decision",
@@ -2932,6 +2949,12 @@
2932
2949
  "fixerAttempts": "Fixer attempts",
2933
2950
  "fixCount": "{attempts}/{max} fix",
2934
2951
  "fixingSuffix": "· fixing…",
2952
+ "fixerTimeline": {
2953
+ "attempt": "Attempt {n}",
2954
+ "completed": "Completed",
2955
+ "failed": "Failed",
2956
+ "addressed": "Addressing"
2957
+ },
2935
2958
  "empty": {
2936
2959
  "title": "No test report yet.",
2937
2960
  "hint": "The report appears once the Tester finishes a pass. While it runs, the step shows live progress on the board."
@@ -2975,6 +2998,7 @@
2975
2998
  "blocking": "({count} blocking) | ({count} blocking)"
2976
2999
  },
2977
3000
  "environment": "Environment",
3001
+ "infrastructure": "Infrastructure",
2978
3002
  "footer": "Scenarios are the areas the Tester chose to exercise (its spec acceptance scenarios). Outcomes and concerns are grouped under them by name.",
2979
3003
  "@screenshotAlt": {
2980
3004
  "description": "Alt text for a captured screenshot thumbnail; {view} is the screen/view name. The literal word 'screenshot' should be localized."
@@ -584,6 +584,23 @@
584
584
  "approved": "Aprobado",
585
585
  "changes_requested": "Cambios solicitados",
586
586
  "rejected": "Rechazado"
587
+ },
588
+ "container": {
589
+ "status": {
590
+ "starting": "Iniciando contenedor…",
591
+ "up": "Contenedor en ejecución",
592
+ "errored": "Error del contenedor",
593
+ "destroyed": "Contenedor liberado"
594
+ },
595
+ "phase": {
596
+ "starting": "Iniciando",
597
+ "clone": "Preparando el espacio de trabajo",
598
+ "agent": "Agente en ejecución",
599
+ "push": "Enviando cambios",
600
+ "done": "Finalizando"
601
+ },
602
+ "id": "Contenedor",
603
+ "url": "Dirección"
587
604
  }
588
605
  },
589
606
  "stepDetail": {
@@ -2845,6 +2862,12 @@
2845
2862
  "fixerAttempts": "Intentos del corrector",
2846
2863
  "fixCount": "{attempts}/{max} corrección",
2847
2864
  "fixingSuffix": "· corrigiendo…",
2865
+ "fixerTimeline": {
2866
+ "attempt": "Intento {n}",
2867
+ "completed": "Completado",
2868
+ "failed": "Falló",
2869
+ "addressed": "Abordando"
2870
+ },
2848
2871
  "empty": {
2849
2872
  "title": "Aún no hay informe de pruebas.",
2850
2873
  "hint": "El informe aparece cuando el Tester termina una pasada. Mientras se ejecuta, el paso muestra el progreso en vivo en el tablero."
@@ -2888,7 +2911,8 @@
2888
2911
  "blocking": "({count} bloqueante) | ({count} bloqueantes)"
2889
2912
  },
2890
2913
  "environment": "Entorno",
2891
- "footer": "Los escenarios son las áreas que el Tester decidió ejercitar (sus escenarios de aceptación de la especificación). Los resultados y las incidencias se agrupan bajo ellos por nombre."
2914
+ "footer": "Los escenarios son las áreas que el Tester decidió ejercitar (sus escenarios de aceptación de la especificación). Los resultados y las incidencias se agrupan bajo ellos por nombre.",
2915
+ "infrastructure": "Infraestructura"
2892
2916
  },
2893
2917
  "visualConfirm": {
2894
2918
  "ariaLabel": "Confirmación visual",
@@ -584,6 +584,23 @@
584
584
  "approved": "Approuvé",
585
585
  "changes_requested": "Modifications demandées",
586
586
  "rejected": "Rejeté"
587
+ },
588
+ "container": {
589
+ "status": {
590
+ "starting": "Démarrage du conteneur…",
591
+ "up": "Conteneur en cours d’exécution",
592
+ "errored": "Erreur du conteneur",
593
+ "destroyed": "Conteneur libéré"
594
+ },
595
+ "phase": {
596
+ "starting": "Démarrage",
597
+ "clone": "Préparation de l’espace de travail",
598
+ "agent": "Agent en cours d’exécution",
599
+ "push": "Envoi des modifications",
600
+ "done": "Finalisation"
601
+ },
602
+ "id": "Conteneur",
603
+ "url": "Adresse"
587
604
  }
588
605
  },
589
606
  "stepDetail": {
@@ -2845,6 +2862,12 @@
2845
2862
  "fixerAttempts": "Tentatives du correcteur",
2846
2863
  "fixCount": "{attempts}/{max} correction",
2847
2864
  "fixingSuffix": "· correction…",
2865
+ "fixerTimeline": {
2866
+ "attempt": "Tentative {n}",
2867
+ "completed": "Terminé",
2868
+ "failed": "Échec",
2869
+ "addressed": "Corrige"
2870
+ },
2848
2871
  "empty": {
2849
2872
  "title": "Aucun rapport de tests pour l'instant.",
2850
2873
  "hint": "Le rapport apparaît une fois que le Testeur a terminé une passe. Pendant son exécution, l'étape affiche la progression en direct sur le tableau."
@@ -2888,7 +2911,8 @@
2888
2911
  "blocking": "({count} bloquante) | ({count} bloquantes)"
2889
2912
  },
2890
2913
  "environment": "Environnement",
2891
- "footer": "Les scénarios sont les domaines que le Testeur a choisi d'éprouver (ses scénarios d'acceptation de la spécification). Les résultats et les réserves y sont regroupés par nom."
2914
+ "footer": "Les scénarios sont les domaines que le Testeur a choisi d'éprouver (ses scénarios d'acceptation de la spécification). Les résultats et les réserves y sont regroupés par nom.",
2915
+ "infrastructure": "Infrastructure"
2892
2916
  },
2893
2917
  "visualConfirm": {
2894
2918
  "ariaLabel": "Confirmation visuelle",
@@ -584,6 +584,23 @@
584
584
  "approved": "Zatwierdzone",
585
585
  "changes_requested": "Zażądano zmian",
586
586
  "rejected": "Odrzucone"
587
+ },
588
+ "container": {
589
+ "status": {
590
+ "starting": "Uruchamianie kontenera…",
591
+ "up": "Kontener działa",
592
+ "errored": "Błąd kontenera",
593
+ "destroyed": "Kontener zwolniony"
594
+ },
595
+ "phase": {
596
+ "starting": "Uruchamianie",
597
+ "clone": "Przygotowywanie obszaru roboczego",
598
+ "agent": "Agent działa",
599
+ "push": "Wysyłanie zmian",
600
+ "done": "Kończenie"
601
+ },
602
+ "id": "Kontener",
603
+ "url": "Adres"
587
604
  }
588
605
  },
589
606
  "stepDetail": {
@@ -2845,6 +2862,12 @@
2845
2862
  "fixerAttempts": "Próby naprawy",
2846
2863
  "fixCount": "{attempts}/{max} naprawa",
2847
2864
  "fixingSuffix": "· naprawianie…",
2865
+ "fixerTimeline": {
2866
+ "attempt": "Próba {n}",
2867
+ "completed": "Ukończono",
2868
+ "failed": "Niepowodzenie",
2869
+ "addressed": "Naprawia"
2870
+ },
2848
2871
  "empty": {
2849
2872
  "title": "Brak raportu z testów.",
2850
2873
  "hint": "Raport pojawi się, gdy Tester zakończy przebieg. W trakcie działania krok pokazuje postęp na żywo na tablicy."
@@ -2888,7 +2911,8 @@
2888
2911
  "blocking": "({count} blokujące) | ({count} blokujące) | ({count} blokujących)"
2889
2912
  },
2890
2913
  "environment": "Środowisko",
2891
- "footer": "Scenariusze to obszary, które Tester postanowił sprawdzić (jego scenariusze akceptacyjne ze specyfikacji). Wyniki i zastrzeżenia są pod nimi grupowane według nazwy."
2914
+ "footer": "Scenariusze to obszary, które Tester postanowił sprawdzić (jego scenariusze akceptacyjne ze specyfikacji). Wyniki i zastrzeżenia są pod nimi grupowane według nazwy.",
2915
+ "infrastructure": "Infrastruktura"
2892
2916
  },
2893
2917
  "visualConfirm": {
2894
2918
  "ariaLabel": "Potwierdzenie wizualne",
@@ -584,6 +584,23 @@
584
584
  "approved": "Затверджено",
585
585
  "changes_requested": "Запитано зміни",
586
586
  "rejected": "Відхилено"
587
+ },
588
+ "container": {
589
+ "status": {
590
+ "starting": "Запуск контейнера…",
591
+ "up": "Контейнер працює",
592
+ "errored": "Помилка контейнера",
593
+ "destroyed": "Контейнер звільнено"
594
+ },
595
+ "phase": {
596
+ "starting": "Запуск",
597
+ "clone": "Підготовка робочого простору",
598
+ "agent": "Агент працює",
599
+ "push": "Надсилання змін",
600
+ "done": "Завершення"
601
+ },
602
+ "id": "Контейнер",
603
+ "url": "Адреса"
587
604
  }
588
605
  },
589
606
  "stepDetail": {
@@ -2845,6 +2862,12 @@
2845
2862
  "fixerAttempts": "Спроби виправлення",
2846
2863
  "fixCount": "{attempts}/{max} виправлення",
2847
2864
  "fixingSuffix": "· виправлення…",
2865
+ "fixerTimeline": {
2866
+ "attempt": "Спроба {n}",
2867
+ "completed": "Завершено",
2868
+ "failed": "Помилка",
2869
+ "addressed": "Виправляє"
2870
+ },
2848
2871
  "empty": {
2849
2872
  "title": "Звіту про тестування ще немає.",
2850
2873
  "hint": "Звіт зʼявиться після того, як Тестувальник завершить прохід. Поки він виконується, крок показує прогрес у реальному часі на дошці."
@@ -2888,7 +2911,8 @@
2888
2911
  "blocking": "({count} блокувальне) | ({count} блокувальні) | ({count} блокувальних)"
2889
2912
  },
2890
2913
  "environment": "Середовище",
2891
- "footer": "Сценарії — це області, які Тестувальник вирішив перевірити (його сценарії приймання зі специфікації). Результати та зауваження групуються під ними за назвою."
2914
+ "footer": "Сценарії — це області, які Тестувальник вирішив перевірити (його сценарії приймання зі специфікації). Результати та зауваження групуються під ними за назвою.",
2915
+ "infrastructure": "Інфраструктура"
2892
2916
  },
2893
2917
  "visualConfirm": {
2894
2918
  "ariaLabel": "Візуальне підтвердження",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cat-factory/app",
3
- "version": "0.50.1",
3
+ "version": "0.51.0",
4
4
  "description": "Reusable Nuxt layer for the Agent Architecture Board SPA (components, stores, composables, pages). Consume it from a thin deployment app via `extends: ['@cat-factory/app']` and point it at your backend with NUXT_PUBLIC_API_BASE. See deploy/frontend for an example.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -34,7 +34,7 @@
34
34
  "valibot": "^1.4.2",
35
35
  "vue": "^3.5.39",
36
36
  "wretch": "^3.0.9",
37
- "@cat-factory/contracts": "0.52.0"
37
+ "@cat-factory/contracts": "0.53.0"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@toad-contracts/testing": "0.3.2",