@cat-factory/app 0.50.1 → 0.51.1
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.
- package/app/components/layout/TranslationWarningBanner.vue +13 -9
- package/app/components/panels/StepContainerStatus.vue +116 -0
- package/app/components/panels/StepMetadataCard.vue +4 -9
- package/app/components/panels/inspector/TaskExecution.vue +19 -6
- package/app/components/pipeline/PipelineProgress.vue +25 -7
- package/app/components/testing/TestReportWindow.vue +129 -4
- package/app/types/execution.ts +2 -0
- package/app/utils/pipelineRender.ts +18 -0
- package/i18n/locales/en.json +24 -0
- package/i18n/locales/es.json +25 -1
- package/i18n/locales/fr.json +25 -1
- package/i18n/locales/pl.json +25 -1
- package/i18n/locales/uk.json +25 -1
- package/package.json +2 -2
|
@@ -1,22 +1,26 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import {
|
|
2
|
+
import { useLocalStorage } from '@vueuse/core'
|
|
3
|
+
import { computed } from 'vue'
|
|
3
4
|
|
|
4
5
|
// Shown whenever the active locale is NOT English: the non-English catalogs are
|
|
5
6
|
// community/AI-provided and may be inaccurate, so warn the user and point them at the
|
|
6
7
|
// repository to report mistakes or open a fix PR. Rendered as a slim full-width strip at
|
|
7
8
|
// the very top (distinct from the centered config-warning cards below it, so they don't
|
|
8
|
-
// overlap).
|
|
9
|
+
// overlap). Dismissal is persisted per-locale in localStorage: once dismissed for a locale
|
|
10
|
+
// it stays hidden across reloads, but switching to a different (separately-translated)
|
|
11
|
+
// locale shows it again, since that catalog is a fresh, independently-translated context.
|
|
9
12
|
const REPO_URL = 'https://github.com/kibertoad/cat-factory'
|
|
10
13
|
|
|
11
14
|
const { t, locale } = useI18n()
|
|
12
15
|
|
|
13
|
-
const
|
|
14
|
-
const show = computed(() => locale.value !== 'en' && !
|
|
16
|
+
const dismissedLocales = useLocalStorage<string[]>('cat-factory:translation-warning-dismissed', [])
|
|
17
|
+
const show = computed(() => locale.value !== 'en' && !dismissedLocales.value.includes(locale.value))
|
|
15
18
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
19
|
+
function dismiss() {
|
|
20
|
+
if (!dismissedLocales.value.includes(locale.value)) {
|
|
21
|
+
dismissedLocales.value = [...dismissedLocales.value, locale.value]
|
|
22
|
+
}
|
|
23
|
+
}
|
|
20
24
|
</script>
|
|
21
25
|
|
|
22
26
|
<template>
|
|
@@ -51,7 +55,7 @@ watch(locale, () => {
|
|
|
51
55
|
size="xs"
|
|
52
56
|
icon="i-lucide-x"
|
|
53
57
|
:aria-label="t('language.warning.dismiss')"
|
|
54
|
-
@click="
|
|
58
|
+
@click="dismiss"
|
|
55
59
|
/>
|
|
56
60
|
</div>
|
|
57
61
|
</Transition>
|
|
@@ -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
|
|
157
|
-
the
|
|
158
|
-
<
|
|
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 {
|
|
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
|
-
|
|
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
|
|
83
|
-
//
|
|
84
|
-
|
|
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
|
-
//
|
|
131
|
-
// no
|
|
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
|
|
360
|
-
agent starts reporting progress -->
|
|
368
|
+
<!-- container cold-boot phase: shown while the container is spinning up. -->
|
|
361
369
|
<div
|
|
362
|
-
v-if="s.
|
|
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
|
|
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>
|
package/app/types/execution.ts
CHANGED
|
@@ -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
|
package/i18n/locales/en.json
CHANGED
|
@@ -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."
|
package/i18n/locales/es.json
CHANGED
|
@@ -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",
|
package/i18n/locales/fr.json
CHANGED
|
@@ -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",
|
package/i18n/locales/pl.json
CHANGED
|
@@ -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",
|
package/i18n/locales/uk.json
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "0.51.1",
|
|
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.
|
|
37
|
+
"@cat-factory/contracts": "0.53.0"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@toad-contracts/testing": "0.3.2",
|