@cat-factory/app 0.77.0 → 0.79.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,62 @@
1
+ <script setup lang="ts">
2
+ // Shared renderer for a gate's failing-check list (`gateFailingCheckSchema[]`): each check
3
+ // links to its GitHub run when a URL is known, with its conclusion. Used by BOTH the CI
4
+ // gate's precheck panel and each per-attempt "handed to the fixer" list in GateResultView,
5
+ // so the link + conclusion-fallback logic lives in one place (they had drifted — the
6
+ // per-attempt copy silently dropped the GitHub link).
7
+ import type { GateFailingCheck } from '~/types/execution'
8
+
9
+ defineProps<{
10
+ checks: GateFailingCheck[]
11
+ // Compact layout for the per-attempt timeline; the fuller card layout is the default
12
+ // (the precheck panel).
13
+ dense?: boolean
14
+ }>()
15
+
16
+ const { t } = useI18n()
17
+ </script>
18
+
19
+ <template>
20
+ <ul :class="dense ? 'space-y-0.5' : 'space-y-1'">
21
+ <li
22
+ v-for="(c, i) in checks"
23
+ :key="`${c.name}-${i}`"
24
+ class="flex items-center"
25
+ :class="
26
+ dense ? 'gap-1.5' : 'gap-2 rounded-md border border-slate-800 bg-slate-950/40 px-3 py-1.5'
27
+ "
28
+ >
29
+ <UIcon
30
+ name="i-lucide-circle-x"
31
+ class="shrink-0 text-rose-400"
32
+ :class="dense ? 'h-3 w-3' : 'h-3.5 w-3.5'"
33
+ />
34
+ <a
35
+ v-if="c.url"
36
+ :href="c.url"
37
+ target="_blank"
38
+ rel="noopener"
39
+ class="group min-w-0 flex-1 truncate text-sky-300 hover:text-sky-200 hover:underline"
40
+ :class="dense ? 'text-[12px]' : 'text-[13px]'"
41
+ :title="t('gates.ci.openOnGithub', { name: c.name })"
42
+ >
43
+ {{ c.name }}
44
+ <UIcon
45
+ name="i-lucide-external-link"
46
+ class="ms-0.5 inline h-3 w-3 opacity-60 group-hover:opacity-100"
47
+ />
48
+ </a>
49
+ <span
50
+ v-else
51
+ class="min-w-0 flex-1 truncate"
52
+ :class="dense ? 'text-[12px] text-slate-300' : 'text-[13px] text-slate-200'"
53
+ >{{ c.name }}</span
54
+ >
55
+ <span
56
+ class="shrink-0 uppercase text-rose-300"
57
+ :class="dense ? 'text-[10px]' : 'text-[11px]'"
58
+ >{{ c.conclusion ?? t('gates.ci.conclusionFallback') }}</span
59
+ >
60
+ </li>
61
+ </ul>
62
+ </template>
@@ -10,10 +10,12 @@ import { agentKindMeta } from '~/utils/catalog'
10
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
+ import AttemptEntryHeader from '~/components/panels/AttemptEntryHeader.vue'
14
+ import GateFailingCheckList from '~/components/gates/GateFailingCheckList.vue'
13
15
 
14
16
  const board = useBoardStore()
15
17
  const execution = useExecutionStore()
16
- const { t, d } = useI18n()
18
+ const { t } = useI18n()
17
19
 
18
20
  // Synchronous window: it reads its state straight off the execution step, so there's
19
21
  // nothing to fetch on open (no `onOpen` loader).
@@ -75,10 +77,6 @@ const OUTCOME_LABELS = computed<Record<GateAttempt['outcome'], string>>(() => ({
75
77
  failed: t('gates.outcome.failed'),
76
78
  }))
77
79
 
78
- function formatClock(ms?: number | null): string | null {
79
- return ms ? d(new Date(ms), 'long') : null
80
- }
81
-
82
80
  /**
83
81
  * The display status — a roll-up of the persisted gate state + the run's status, so the
84
82
  * window reads as a conclusion rather than raw fields:
@@ -308,35 +306,7 @@ const conflictVerdict = computed(() => {
308
306
  <h3 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
309
307
  {{ t('gates.ci.failingChecks') }}
310
308
  </h3>
311
- <ul v-if="failingChecks.length" class="space-y-1">
312
- <li
313
- v-for="(c, i) in failingChecks"
314
- :key="`${c.name}-${i}`"
315
- class="flex items-center gap-2 rounded-md border border-slate-800 bg-slate-950/40 px-3 py-1.5"
316
- >
317
- <UIcon name="i-lucide-circle-x" class="h-3.5 w-3.5 shrink-0 text-rose-400" />
318
- <a
319
- v-if="c.url"
320
- :href="c.url"
321
- target="_blank"
322
- rel="noopener"
323
- class="group min-w-0 flex-1 truncate text-[13px] text-sky-300 hover:text-sky-200 hover:underline"
324
- :title="t('gates.ci.openOnGithub', { name: c.name })"
325
- >
326
- {{ c.name }}
327
- <UIcon
328
- name="i-lucide-external-link"
329
- class="ms-0.5 inline h-3 w-3 opacity-60 group-hover:opacity-100"
330
- />
331
- </a>
332
- <span v-else class="min-w-0 flex-1 truncate text-[13px] text-slate-200">{{
333
- c.name
334
- }}</span>
335
- <span class="shrink-0 text-[11px] uppercase text-rose-300">
336
- {{ c.conclusion ?? t('gates.ci.conclusionFallback') }}
337
- </span>
338
- </li>
339
- </ul>
309
+ <GateFailingCheckList v-if="failingChecks.length" :checks="failingChecks" />
340
310
  <p v-else class="text-[13px] leading-relaxed text-slate-300">
341
311
  {{ gate.lastFailureSummary || t('gates.ci.failureFallback') }}
342
312
  </p>
@@ -389,26 +359,47 @@ const conflictVerdict = computed(() => {
389
359
  :key="a.attempt"
390
360
  class="rounded-md border border-slate-800 bg-slate-950/40 px-3 py-2"
391
361
  >
392
- <div class="flex items-center gap-2">
393
- <span class="text-[12px] font-semibold text-slate-200">{{
394
- t('gates.attempt', { number: a.attempt })
395
- }}</span>
396
- <UBadge
397
- :color="a.outcome === 'failed' ? 'error' : 'neutral'"
398
- variant="subtle"
399
- size="sm"
400
- >{{ OUTCOME_LABELS[a.outcome] }}</UBadge
362
+ <AttemptEntryHeader
363
+ :label="t('gates.attempt', { number: a.attempt })"
364
+ :outcome="a.outcome"
365
+ :outcome-label="OUTCOME_LABELS[a.outcome]"
366
+ :at="a.at"
367
+ date-format="long"
368
+ />
369
+ <!-- What this round was asked to fix: the instructions the gate handed the
370
+ helper (the failing-check summary / conflict reason / review comments),
371
+ plus the structured red checks for the CI gate. -->
372
+ <div
373
+ v-if="a.instructions || (a.failingChecks && a.failingChecks.length)"
374
+ class="mt-1.5"
375
+ >
376
+ <p class="text-[11px] text-slate-500">
377
+ {{ t('gates.attemptInstructions', { helper: helperMeta.label }) }}
378
+ </p>
379
+ <GateFailingCheckList
380
+ v-if="a.failingChecks && a.failingChecks.length"
381
+ class="mt-1"
382
+ :checks="a.failingChecks"
383
+ dense
384
+ />
385
+ <p
386
+ v-else-if="a.instructions"
387
+ class="mt-1 whitespace-pre-wrap text-[12px] leading-relaxed text-slate-300"
401
388
  >
402
- <span v-if="formatClock(a.at)" class="ms-auto text-[11px] text-slate-500">{{
403
- formatClock(a.at)
404
- }}</span>
389
+ {{ a.instructions }}
390
+ </p>
405
391
  </div>
406
- <p
407
- v-if="a.summary"
408
- class="mt-1 whitespace-pre-wrap text-[12px] leading-relaxed text-slate-400"
409
- >
410
- {{ a.summary }}
411
- </p>
392
+ <!-- The helper's own report of what it did / what remains. -->
393
+ <template v-if="a.summary">
394
+ <p class="mt-1.5 text-[11px] text-slate-500">
395
+ {{ t('gates.attemptReport', { helper: helperMeta.label }) }}
396
+ </p>
397
+ <p
398
+ class="mt-1 whitespace-pre-wrap text-[12px] leading-relaxed text-slate-400"
399
+ >
400
+ {{ a.summary }}
401
+ </p>
402
+ </template>
412
403
  </li>
413
404
  </ol>
414
405
  </section>
@@ -20,6 +20,7 @@ const documents = useDocumentsStore()
20
20
  const tasks = useTasksStore()
21
21
  const tracker = useTrackerStore()
22
22
  const releaseHealth = useReleaseHealthStore()
23
+ const packageRegistries = usePackageRegistriesStore()
23
24
  const userSecrets = useUserSecretsStore()
24
25
  const apiKeys = useApiKeysStore()
25
26
  const workspace = useWorkspaceStore()
@@ -49,6 +50,7 @@ watch(
49
50
  if (isOpen) {
50
51
  query.value = ''
51
52
  void releaseHealth.ensureLoaded().catch(() => {})
53
+ void packageRegistries.ensureLoaded().catch(() => {})
52
54
  void userSecrets.load().catch(() => {})
53
55
  // Drives the OpenRouter row's "Key connected" badge.
54
56
  if (workspace.workspaceId) void apiKeys.load(workspace.workspaceId).catch(() => {})
@@ -256,6 +258,27 @@ const groups = computed<IntegrationGroup[]>(() => {
256
258
  })
257
259
  }
258
260
 
261
+ // --- Development (private package registries) -------------------------------
262
+ // Gated like observability: hidden until a probe confirms the module is wired
263
+ // (`available === true`), so an unconfigured backend doesn't show a dead row.
264
+ if (packageRegistries.available) {
265
+ const hasEntries = packageRegistries.entries.length > 0
266
+ out.push({
267
+ title: t('layout.integrationsHub.groups.development'),
268
+ items: [
269
+ {
270
+ key: 'package-registries',
271
+ icon: 'i-lucide-package',
272
+ label: t('layout.integrationsHub.items.packageRegistries.label'),
273
+ description: t('layout.integrationsHub.items.packageRegistries.description'),
274
+ status: hasEntries ? t('layout.integrationsHub.status.connected') : undefined,
275
+ connected: hasEntries,
276
+ onClick: () => go(ui.openPackageRegistries),
277
+ },
278
+ ],
279
+ })
280
+ }
281
+
259
282
  // NOTE: Infrastructure (agent-container execution + Tester environments + the local-mode
260
283
  // warm pool/checkout) is no longer listed here — it moved to its OWN top-level navbar menu
261
284
  // (SideBar → "Infrastructure" → the tabbed Infrastructure window). See `ui.openInfrastructure`.
@@ -7,6 +7,8 @@ import StepRestartControl from '~/components/panels/StepRestartControl.vue'
7
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
+ import FrontendBindingsResolved from '~/components/panels/inspector/FrontendBindingsResolved.vue'
11
+ import { UI_TESTER_AGENT_KIND } from '@cat-factory/contracts'
10
12
  import ProvisioningLogsDrawer from '~/components/provisioning/ProvisioningLogsDrawer.vue'
11
13
  import IterationCapPrompt from '~/components/pipeline/IterationCapPrompt.vue'
12
14
  import { useStepTimer } from '~/composables/useStepTimer'
@@ -58,6 +60,24 @@ const testPhase = computed(() => step.value?.test ?? null)
58
60
  // coder consume it), so the panel shows its spinning-up/running/shutdown/errored state.
59
61
  const stepEnvironment = computed(() => step.value?.environment ?? null)
60
62
 
63
+ // For a frontend UI-test step (`tester-ui`): the enclosing `frontend` frame's backend-binding
64
+ // config, so the detail can project how each env var resolved (live URL | mocked) — rendered from
65
+ // the FROZEN bindings the engine stamped on the run (`instance.frontendBindings`), so a finished
66
+ // run shows what it actually drove against rather than re-resolving against current live state.
67
+ const frontendFrame = computed(() => (block.value ? board.serviceOf(block.value) : undefined))
68
+ const isFrontendFrame = computed(() => frontendFrame.value?.type === 'frontend')
69
+ const frontendConfig = computed(() =>
70
+ step.value?.agentKind === UI_TESTER_AGENT_KIND && isFrontendFrame.value
71
+ ? (frontendFrame.value!.frontendConfig ?? null)
72
+ : null,
73
+ )
74
+ // The frozen start-time resolution the tester ran against (absent for a non-frontend / pre-6b run).
75
+ const frontendBindings = computed(() => instance.value?.frontendBindings ?? [])
76
+ // The run-start advisories the engine stamped on the run (duplicate env vars / partially-mocked
77
+ // services) are a whole-RUN fact, so surface them on ANY step detail of a frontend-frame run, not
78
+ // only the `tester-ui` step — a duplicate-env-var note shouldn't be invisible from the coder step.
79
+ const runNotes = computed(() => (isFrontendFrame.value ? (instance.value?.notes ?? []) : []))
80
+
61
81
  // The run's infrastructure attempts (container/runner/env spin-up + tear-down), behind
62
82
  // a toggle. This is the surface that makes the per-run `container` log rows + the
63
83
  // executionId filter visible — most useful when the run failed to start a container.
@@ -345,6 +365,27 @@ async function copyOutput() {
345
365
  errored + the exact error), when this step runs against one -->
346
366
  <EnvironmentStatusPanel v-if="stepEnvironment" :environment="stepEnvironment" />
347
367
 
368
+ <!-- frontend UI-test: how the frame's backend bindings resolved (env var →
369
+ live URL | mocked) + the run-start advisories (duplicate env vars /
370
+ partially-mocked services) the engine stamped on the run. Rendered from the
371
+ FROZEN start-time bindings so a finished run shows what it actually drove
372
+ against, not a live re-resolution. -->
373
+ <FrontendBindingsResolved
374
+ v-if="frontendConfig"
375
+ :config="frontendConfig"
376
+ :resolved="frontendBindings"
377
+ />
378
+ <ul v-if="runNotes.length" class="space-y-1" data-testid="run-notes">
379
+ <li
380
+ v-for="(note, i) in runNotes"
381
+ :key="i"
382
+ class="flex items-start gap-1.5 text-[11px] leading-snug text-amber-300/80"
383
+ >
384
+ <UIcon name="i-lucide-info" class="mt-0.5 h-3.5 w-3.5 shrink-0" />
385
+ <span>{{ note }}</span>
386
+ </li>
387
+ </ul>
388
+
348
389
  <!-- this run's infrastructure attempts (container/runner/env spin-up +
349
390
  tear-down): the surface for the per-run container log rows + the exact
350
391
  provider error, behind a toggle (most useful on a failed-to-start run) -->
@@ -0,0 +1,36 @@
1
+ <script setup lang="ts">
2
+ // Shared header row for an attempt-timeline entry. The polling-gate helper attempts
3
+ // (GateResultView) and the Tester's fixer rounds (TestReportWindow) render the same chrome —
4
+ // a leading label, an outcome badge, and a timestamp — differing only in the optional icon,
5
+ // the resolved label strings, and the date format. The per-attempt body stays the caller's.
6
+ defineProps<{
7
+ label: string
8
+ outcome: 'completed' | 'failed'
9
+ outcomeLabel: string
10
+ at?: number | null
11
+ dateFormat?: 'short' | 'long'
12
+ icon?: string
13
+ iconClass?: string
14
+ }>()
15
+
16
+ const { d } = useI18n()
17
+
18
+ function formatClock(ms: number | null | undefined, fmt: 'short' | 'long'): string | null {
19
+ return ms ? d(new Date(ms), fmt) : null
20
+ }
21
+ </script>
22
+
23
+ <template>
24
+ <div class="flex items-center gap-2">
25
+ <UIcon v-if="icon" :name="icon" class="h-3.5 w-3.5 shrink-0" :class="iconClass" />
26
+ <span class="text-[13px] font-medium text-slate-200">{{ label }}</span>
27
+ <UBadge :color="outcome === 'failed' ? 'error' : 'neutral'" variant="subtle" size="sm">{{
28
+ outcomeLabel
29
+ }}</UBadge>
30
+ <span
31
+ v-if="formatClock(at, dateFormat ?? 'short')"
32
+ class="ms-auto text-[11px] text-slate-500"
33
+ >{{ formatClock(at, dateFormat ?? 'short') }}</span
34
+ >
35
+ </div>
36
+ </template>
@@ -0,0 +1,111 @@
1
+ <script setup lang="ts">
2
+ import { computed, onMounted } from 'vue'
3
+ import {
4
+ duplicateBindingEnvVars,
5
+ resolveFrontendBindings,
6
+ type FrontendBackendBinding,
7
+ type FrontendConfig,
8
+ type ResolvedFrontendBinding,
9
+ } from '@cat-factory/contracts'
10
+
11
+ // The resolution of a frontend frame's backend bindings — each env var → a bound service's live
12
+ // ephemeral URL, or WireMock. Two modes, same view:
13
+ // - **Live** (frame inspector, `resolved` omitted): resolves against the workspace's CURRENT env
14
+ // handles (fetched once via the environments store), so the operator sees how a run would
15
+ // resolve RIGHT NOW. Feeds the SAME pure helpers the backend uses so it can't drift.
16
+ // - **Projected** (`tester-ui` run/step detail, `resolved` provided): renders the FROZEN
17
+ // start-time bindings the engine stamped on the run, so a finished run shows what it ACTUALLY
18
+ // drove against — truthful even after the underlying envs are torn down (no live re-read).
19
+ // Also surfaces the duplicate-env-var misconfiguration in live mode (projected mode leaves that to
20
+ // the run-start note, which owns the frozen advisory).
21
+ const props = defineProps<{ config: FrontendConfig; resolved?: ResolvedFrontendBinding[] }>()
22
+
23
+ const environments = useEnvironmentsStore()
24
+ const board = useBoardStore()
25
+ const { t } = useI18n()
26
+
27
+ const projected = computed(() => props.resolved !== undefined)
28
+
29
+ // Live mode refreshes the env handles when this view opens so a just-provisioned service shows as
30
+ // live; a projected snapshot needs no live read.
31
+ onMounted(() => {
32
+ if (!projected.value) void environments.load()
33
+ })
34
+
35
+ // The duplicate advisory is config-derived; in projected mode the run-start note owns it (frozen
36
+ // at start), so don't re-derive it here against a possibly-since-edited config.
37
+ const duplicates = computed(() => (projected.value ? [] : duplicateBindingEnvVars(props.config)))
38
+
39
+ // Each resolved binding + the display metadata a bare {envVar, serviceUrl} can't carry: whether
40
+ // a mocked upstream was a `mock` source or a `service` with no live env, and the bound service's
41
+ // title. Joined off the LAST config binding per envVar (matching `resolveFrontendBindings`'
42
+ // last-wins dedup), so the extra labels stay in step with the canonical resolution.
43
+ const rows = computed(() => {
44
+ const resolved =
45
+ props.resolved ??
46
+ resolveFrontendBindings(props.config, environments.liveServiceEnvUrls(props.config))
47
+ const lastByEnvVar = new Map<string, FrontendBackendBinding>()
48
+ for (const b of props.config.backendBindings) {
49
+ const key = b.envVar.trim()
50
+ if (key) lastByEnvVar.set(key, b)
51
+ }
52
+ return resolved.map((r) => {
53
+ const source = lastByEnvVar.get(r.envVar)?.source
54
+ const serviceFrameId = source?.kind === 'service' ? source.serviceBlockId : undefined
55
+ return {
56
+ envVar: r.envVar,
57
+ serviceUrl: r.serviceUrl,
58
+ kind: r.serviceUrl ? 'live' : source?.kind === 'service' ? 'service-offline' : 'mock',
59
+ serviceTitle: serviceFrameId
60
+ ? (board.getBlock(serviceFrameId)?.title ?? serviceFrameId)
61
+ : undefined,
62
+ } as const
63
+ })
64
+ })
65
+ </script>
66
+
67
+ <template>
68
+ <div v-if="rows.length || duplicates.length" class="space-y-1.5" data-testid="frontend-resolved">
69
+ <div class="text-[11px] font-semibold uppercase tracking-wide text-slate-500">
70
+ {{ t('inspector.frontendConfig.resolved.title') }}
71
+ </div>
72
+
73
+ <p
74
+ v-if="duplicates.length"
75
+ class="text-[11px] leading-snug text-amber-300/80"
76
+ data-testid="frontend-resolved-duplicates"
77
+ >
78
+ {{ t('inspector.frontendConfig.resolved.duplicateWarning', { vars: duplicates.join(', ') }) }}
79
+ </p>
80
+
81
+ <ul v-if="rows.length" class="space-y-0.5">
82
+ <li
83
+ v-for="row in rows"
84
+ :key="row.envVar"
85
+ class="flex items-baseline gap-1.5 text-[11px] leading-snug"
86
+ data-testid="frontend-resolved-row"
87
+ >
88
+ <span
89
+ class="mt-1 h-1.5 w-1.5 shrink-0 rounded-full"
90
+ :class="{
91
+ 'bg-emerald-400': row.kind === 'live',
92
+ 'bg-amber-400': row.kind === 'service-offline',
93
+ 'bg-slate-500': row.kind === 'mock',
94
+ }"
95
+ />
96
+ <span class="font-mono text-slate-300">{{ row.envVar }}</span>
97
+ <span class="text-slate-600">→</span>
98
+ <template v-if="row.kind === 'live'">
99
+ <span class="truncate font-mono text-emerald-300/90">{{ row.serviceUrl }}</span>
100
+ <span v-if="row.serviceTitle" class="text-slate-500">({{ row.serviceTitle }})</span>
101
+ </template>
102
+ <span v-else-if="row.kind === 'service-offline'" class="text-amber-300/80">
103
+ {{ t('inspector.frontendConfig.resolved.serviceOffline', { service: row.serviceTitle }) }}
104
+ </span>
105
+ <span v-else class="text-slate-500">
106
+ {{ t('inspector.frontendConfig.resolved.mock') }}
107
+ </span>
108
+ </li>
109
+ </ul>
110
+ </div>
111
+ </template>
@@ -10,6 +10,7 @@ import type {
10
10
  FrontendServeMode,
11
11
  PreviewStatus,
12
12
  } from '~/types/domain'
13
+ import FrontendBindingsResolved from '~/components/panels/inspector/FrontendBindingsResolved.vue'
13
14
 
14
15
  // Frontend-frame (`type: 'frontend'`) configuration: how to build, serve, and mock this
15
16
  // frontend for a self-contained UI test (+ an optional browsable preview on local/node),
@@ -625,6 +626,13 @@ onUnmounted(() => preview.stopPolling(props.block.id))
625
626
  <div v-else class="text-[11px] text-slate-500">
626
627
  {{ t('inspector.frontendConfig.bindings.empty') }}
627
628
  </div>
629
+
630
+ <!-- How the bindings resolve RIGHT NOW: each env var → a bound service's live ephemeral
631
+ URL, or WireMock — plus the duplicate-env-var warning. The same view a UI-test run
632
+ would resolve against (shared helpers), so what you see is what a run will drive. -->
633
+ <div class="border-t border-slate-800/60 pt-2">
634
+ <FrontendBindingsResolved :config="config" />
635
+ </div>
628
636
  </div>
629
637
  </div>
630
638