@cat-factory/app 0.7.2 → 0.7.4

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.
@@ -1,7 +1,13 @@
1
1
  <script setup lang="ts">
2
2
  import type { AgentState } from '~/types/domain'
3
3
  import { agentKindMeta } from '~/utils/catalog'
4
- import { subtaskIconClass, isFailedStep, FAILED_STEP_META } from '~/utils/pipelineRender'
4
+ import {
5
+ subtaskIconClass,
6
+ isFailedStep,
7
+ FAILED_STEP_META,
8
+ gateCompanionFor,
9
+ COMPANION_STATE_META,
10
+ } from '~/utils/pipelineRender'
5
11
  import { lodAtLeast } from '~/composables/useSemanticZoom'
6
12
 
7
13
  // Spatial drill-down inside a task card: at the `steps` zoom band the task's
@@ -24,6 +30,13 @@ const steps = computed(() => instance.value?.steps ?? [])
24
30
  // `working`) must stop spinning, matching the failure card the task card shows.
25
31
  const runFailed = computed(() => instance.value?.status === 'failed')
26
32
 
33
+ // The conditionally-run companion (if any) each step drives — the polling gates'
34
+ // helper (ci → ci-fixer, conflicts → conflict-resolver) or the Tester's fixer — with
35
+ // its possible/running/completed/skipped state. The board drill-down shows it the same
36
+ // way the inspector + focus pipeline do, so a gate working its helper reads as active
37
+ // (spinning "Running") rather than a frozen subtask list.
38
+ const companionByStep = computed(() => steps.value.map((s) => gateCompanionFor(s, runFailed.value)))
39
+
27
40
  // Expand the pipeline list only when zoomed in far enough AND the board driver
28
41
  // permits this card — on-screen, and the centre-most of any cards that would
29
42
  // otherwise overlap (see useTaskExpansion) — so deep-zoom expansions don't pile up.
@@ -132,6 +145,29 @@ const ITEM_ICON: Record<string, string> = {
132
145
  />
133
146
  </div>
134
147
 
148
+ <!-- conditionally-run companion (the gate's ci-fixer / conflict-resolver, or the
149
+ Tester's fixer): a compact running/ran/skipped line, so a gate that's working
150
+ its helper reads as actively fixing rather than a frozen subtask list. -->
151
+ <div v-if="companionByStep[i]" class="mt-1 flex items-center gap-1 text-[9px]">
152
+ <UIcon
153
+ :name="agentKindMeta(companionByStep[i]!.kind).icon"
154
+ class="h-2.5 w-2.5 shrink-0"
155
+ :class="[
156
+ COMPANION_STATE_META[companionByStep[i]!.state].text,
157
+ companionByStep[i]!.state === 'running' ? 'animate-spin' : '',
158
+ ]"
159
+ />
160
+ <span class="truncate text-slate-400">
161
+ {{ agentKindMeta(companionByStep[i]!.kind).label }}
162
+ </span>
163
+ <span
164
+ class="ml-auto shrink-0"
165
+ :class="COMPANION_STATE_META[companionByStep[i]!.state].text"
166
+ >
167
+ {{ COMPANION_STATE_META[companionByStep[i]!.state].label }}
168
+ </span>
169
+ </div>
170
+
135
171
  <!-- deepest band: the actual todo list (done / in-progress / pending) -->
136
172
  <ul v-if="showItems && s.subtasks?.items?.length" class="mt-1 space-y-0.5">
137
173
  <li
@@ -9,10 +9,10 @@ import { computed } from 'vue'
9
9
  import { agentKindMeta } from '~/utils/catalog'
10
10
  import type { GateStepState } from '~/types/execution'
11
11
  import StepRestartControl from '~/components/panels/StepRestartControl.vue'
12
+ import StepRunMeta from '~/components/panels/StepRunMeta.vue'
12
13
 
13
14
  const board = useBoardStore()
14
15
  const execution = useExecutionStore()
15
- const ui = useUiStore()
16
16
 
17
17
  // Synchronous window: it reads its state straight off the execution step, so there's
18
18
  // nothing to fetch on open (no `onOpen` loader).
@@ -44,15 +44,6 @@ function formatClock(ms?: number | null): string | null {
44
44
  return ms ? new Date(ms).toLocaleString() : null
45
45
  }
46
46
 
47
- // The run id (execution instance id) this gate belongs to — copyable for support /
48
- // log lookups, and a jump into the run's observability view.
49
- async function copyRunId() {
50
- if (instanceId.value) await navigator.clipboard?.writeText(instanceId.value)
51
- }
52
- function openObservability() {
53
- if (instanceId.value) ui.openObservability(instanceId.value)
54
- }
55
-
56
47
  /**
57
48
  * The display status — a roll-up of the persisted gate state + the run's status, so the
58
49
  * window reads as a conclusion rather than raw fields:
@@ -258,19 +249,16 @@ const conflictVerdict = computed(() => {
258
249
  >
259
250
  {{ gate.lastFailureSummary }}
260
251
  </p>
261
- <p class="mt-2 text-[11px] leading-relaxed text-slate-500">
262
- GitHub's API doesn't expose a file-level conflict list, so the resolver inspects
263
- the branch directly.<template v-if="prUrl">
264
- For the exact conflicting hunks, open the
265
- <a
266
- :href="prUrl"
267
- target="_blank"
268
- rel="noopener"
269
- class="text-sky-300 hover:text-sky-200 hover:underline"
270
- >pull request on GitHub</a
271
- >.</template
272
- >
273
- </p>
252
+ <a
253
+ v-if="prUrl"
254
+ :href="prUrl"
255
+ target="_blank"
256
+ rel="noopener"
257
+ class="mt-2 inline-flex items-center gap-1 text-[12px] text-sky-300 hover:text-sky-200 hover:underline"
258
+ >
259
+ View pull request on GitHub
260
+ <UIcon name="i-lucide-external-link" class="h-3 w-3" />
261
+ </a>
274
262
  </template>
275
263
 
276
264
  <!-- Attempt history (both gates): what each helper run did and how it ended. -->
@@ -312,7 +300,7 @@ const conflictVerdict = computed(() => {
312
300
 
313
301
  <!-- Sidebar: gate state -->
314
302
  <aside
315
- class="hidden w-56 shrink-0 flex-col gap-4 border-l border-slate-800 bg-slate-900/50 px-4 py-4 lg:flex"
303
+ class="hidden w-60 shrink-0 flex-col gap-4 border-l border-slate-800 bg-slate-900/50 px-4 py-4 lg:flex"
316
304
  >
317
305
  <div v-if="gate">
318
306
  <h4 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
@@ -348,32 +336,17 @@ const conflictVerdict = computed(() => {
348
336
  <p class="font-mono text-[12px] text-slate-300">{{ shortSha }}</p>
349
337
  </div>
350
338
 
351
- <div v-if="step?.model">
352
- <h4 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
353
- Model
354
- </h4>
355
- <p class="break-all text-[12px] text-slate-300">{{ step.model }}</p>
356
- </div>
357
-
358
- <div v-if="instanceId">
359
- <h4 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
360
- Run
361
- </h4>
362
- <p
363
- class="cursor-pointer break-all font-mono text-[12px] text-slate-400 hover:text-slate-200"
364
- :title="`${instanceId} — click to copy`"
365
- @click="copyRunId"
366
- >
367
- {{ instanceId }}
368
- </p>
369
- <button
370
- class="mt-1 inline-flex items-center gap-1 text-[11px] text-sky-300 hover:text-sky-200"
371
- @click="openObservability"
372
- >
373
- <UIcon name="i-lucide-activity" class="h-3 w-3" />
374
- Open observability
375
- </button>
376
- </div>
339
+ <!-- Shared run metadata + embedded observability (model, run id, timing,
340
+ model-activity rollup) identical to the agent step detail. -->
341
+ <StepRunMeta
342
+ v-if="step"
343
+ :step="step"
344
+ :instance-id="instanceId ?? undefined"
345
+ :step-number="stepIndex === null ? undefined : stepIndex + 1"
346
+ :total-steps="instance?.steps.length"
347
+ :run-failed="instance?.status === 'failed'"
348
+ :failure-at="instance?.failure?.occurredAt"
349
+ />
377
350
 
378
351
  <p class="mt-auto text-[10px] leading-relaxed text-slate-600">
379
352
  A gate runs a programmatic precheck and only spins up the
@@ -0,0 +1,49 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import type { StepMetrics } from '~/types/execution'
4
+ import StepMetricsBar from '~/components/observability/StepMetricsBar.vue'
5
+
6
+ // The shared "Model activity" block: the LLM observability rollup (StepMetricsBar) under
7
+ // a labelled header with a "View all calls →" link into the full per-call panel. Used by
8
+ // every step surface that shows a single step's metrics (the step metadata card, the
9
+ // gate / tester result windows) so the embedded-observability treatment can't drift.
10
+ // The "View all calls →" link opens the run-level panel, so it appears for any step that
11
+ // belongs to a run — including a gate, whose programmatic precheck records no per-step
12
+ // calls (the bar is omitted, but the link still reaches the helper agents' calls). Renders
13
+ // nothing only when there's neither a run to inspect nor any recorded calls.
14
+ const props = defineProps<{
15
+ metrics?: StepMetrics | null
16
+ /** The run whose per-call panel the header link / bar click opens. */
17
+ instanceId?: string
18
+ }>()
19
+
20
+ const ui = useUiStore()
21
+ const hasCalls = computed(() => !!props.metrics && props.metrics.calls > 0)
22
+
23
+ function openObservability() {
24
+ if (props.instanceId) ui.openObservability(props.instanceId)
25
+ }
26
+ </script>
27
+
28
+ <template>
29
+ <div v-if="instanceId || hasCalls">
30
+ <div class="mb-1 flex items-center justify-between">
31
+ <span class="text-[11px] font-semibold uppercase tracking-wide text-slate-500">
32
+ Model activity
33
+ </span>
34
+ <button
35
+ v-if="instanceId"
36
+ class="text-[11px] text-sky-400 hover:text-sky-300"
37
+ @click="openObservability"
38
+ >
39
+ View all calls →
40
+ </button>
41
+ </div>
42
+ <StepMetricsBar
43
+ v-if="hasCalls && metrics"
44
+ :metrics="metrics"
45
+ clickable
46
+ @inspect="openObservability"
47
+ />
48
+ </div>
49
+ </template>
@@ -109,7 +109,7 @@ function exportJson() {
109
109
  <Transition name="obs-fade">
110
110
  <div
111
111
  v-if="open"
112
- class="fixed inset-0 z-50 flex flex-col bg-slate-950/96 backdrop-blur-sm"
112
+ class="fixed inset-0 z-[60] flex flex-col bg-slate-950/96 backdrop-blur-sm"
113
113
  role="dialog"
114
114
  aria-modal="true"
115
115
  >
@@ -2,7 +2,7 @@
2
2
  import { computed } from 'vue'
3
3
  import type { AgentState, PipelineStep, CompanionVerdict } from '~/types/execution'
4
4
  import { subtaskIconClass } from '~/utils/pipelineRender'
5
- import StepMetricsBar from '~/components/observability/StepMetricsBar.vue'
5
+ import StepModelActivity from '~/components/observability/StepModelActivity.vue'
6
6
 
7
7
  // The step's metadata card body: state/timing/model/run id, the container cold-boot
8
8
  // phase, the live subtask breakdown, the LLM observability rollup, the applied
@@ -20,7 +20,6 @@ const props = defineProps<{
20
20
  latestVerdict: CompanionVerdict | null
21
21
  }>()
22
22
 
23
- const ui = useUiStore()
24
23
  const models = useModelsStore()
25
24
 
26
25
  const STATE_META: Record<AgentState, { label: string; color: string }> = {
@@ -56,10 +55,6 @@ async function copyRunId() {
56
55
  const id = props.step.runId ?? props.instanceId
57
56
  if (id) await navigator.clipboard?.writeText(id)
58
57
  }
59
-
60
- function openObservability() {
61
- if (props.instanceId) ui.openObservability(props.instanceId)
62
- }
63
58
  </script>
64
59
 
65
60
  <template>
@@ -169,16 +164,9 @@ function openObservability() {
169
164
  </div>
170
165
 
171
166
  <!-- LLM observability rollup (tokens, output-limit headroom,
172
- transport-vs-execution); click to open the full per-call panel -->
173
- <div v-if="step.metrics && step.metrics.calls > 0" class="mt-4">
174
- <div class="mb-1 flex items-center justify-between">
175
- <span class="text-[11px] uppercase tracking-wide text-slate-500"> Model activity </span>
176
- <button class="text-[11px] text-sky-400 hover:text-sky-300" @click="openObservability">
177
- View all calls →
178
- </button>
179
- </div>
180
- <StepMetricsBar :metrics="step.metrics" clickable @inspect="openObservability" />
181
- </div>
167
+ transport-vs-execution); click to open the full per-call panel. Self-gates: the
168
+ "View all calls →" link shows for any run, the metrics bar only when calls exist. -->
169
+ <StepModelActivity class="mt-4" :metrics="step.metrics" :instance-id="instanceId" />
182
170
 
183
171
  <!-- standards (prompt fragments) folded into this step -->
184
172
  <div v-if="step.selectedFragmentIds && step.selectedFragmentIds.length" class="mt-4">
@@ -0,0 +1,105 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import type { PipelineStep } from '~/types/execution'
4
+ import { useStepTimer } from '~/composables/useStepTimer'
5
+ import StepModelActivity from '~/components/observability/StepModelActivity.vue'
6
+
7
+ // Shared run-metadata + observability block for the step-backed result windows
8
+ // (the CI/conflicts gate, the tester report). It carries the facts every step has in
9
+ // common — step position, live duration, model, run id, and the LLM model-activity
10
+ // rollup — so each window keeps only its own bespoke detail (the gate's verdict, the
11
+ // tester's scenarios) and the universal "which run is this / how did the model do"
12
+ // facts read the same everywhere. Laid out as a stack of labelled fields to drop into
13
+ // a window's sidebar; the canonical full-width version lives in StepMetadataCard.
14
+ const props = defineProps<{
15
+ step: PipelineStep
16
+ /** The enclosing run; copyable + opens the per-call observability panel. */
17
+ instanceId?: string
18
+ /** 1-based position in the pipeline, shown as "N of M" when both are given. */
19
+ stepNumber?: number
20
+ totalSteps?: number
21
+ /** The run failed: freezes the clock and reports a mid-flight step honestly. */
22
+ runFailed?: boolean
23
+ /** Epoch ms the run failed, so the frozen duration is the failure time. */
24
+ failureAt?: number | null
25
+ }>()
26
+
27
+ const models = useModelsStore()
28
+
29
+ const { isRunning, durationLabel } = useStepTimer({
30
+ step: () => props.step,
31
+ runFailed: () => props.runFailed ?? false,
32
+ failureAt: () => props.failureAt,
33
+ })
34
+
35
+ const modelLabel = computed(() => (props.step.model ? models.labelForRef(props.step.model) : null))
36
+ const runId = computed(() => props.step.runId ?? props.instanceId ?? null)
37
+
38
+ function formatClock(ms?: number | null): string | null {
39
+ return ms ? new Date(ms).toLocaleString() : null
40
+ }
41
+
42
+ async function copyRunId() {
43
+ if (runId.value) await navigator.clipboard?.writeText(runId.value)
44
+ }
45
+ </script>
46
+
47
+ <template>
48
+ <div class="flex flex-col gap-4">
49
+ <div v-if="stepNumber && totalSteps">
50
+ <h4 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">Step</h4>
51
+ <p class="text-[12px] text-slate-300">{{ stepNumber }} of {{ totalSteps }}</p>
52
+ </div>
53
+
54
+ <div v-if="durationLabel">
55
+ <h4 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
56
+ Duration
57
+ </h4>
58
+ <p class="flex items-center gap-1.5 text-[12px] tabular-nums text-slate-300">
59
+ <UIcon
60
+ v-if="isRunning"
61
+ name="i-lucide-loader-circle"
62
+ class="h-3 w-3 animate-spin text-indigo-400"
63
+ />
64
+ {{ durationLabel }}
65
+ <span v-if="isRunning" class="text-[11px] text-slate-500">elapsed</span>
66
+ </p>
67
+ </div>
68
+
69
+ <div v-if="formatClock(step.startedAt)">
70
+ <h4 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">Started</h4>
71
+ <p class="text-[12px] text-slate-300">{{ formatClock(step.startedAt) }}</p>
72
+ </div>
73
+
74
+ <div v-if="formatClock(step.finishedAt)">
75
+ <h4 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
76
+ Finished
77
+ </h4>
78
+ <p class="text-[12px] text-slate-300">{{ formatClock(step.finishedAt) }}</p>
79
+ </div>
80
+
81
+ <div v-if="step.model">
82
+ <h4 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">Model</h4>
83
+ <p class="break-all text-[12px] text-slate-300" :title="step.model">
84
+ {{ modelLabel ?? step.model }}
85
+ </p>
86
+ </div>
87
+
88
+ <div v-if="runId">
89
+ <h4 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">Run</h4>
90
+ <p
91
+ class="cursor-pointer break-all font-mono text-[12px] text-slate-400 hover:text-slate-200"
92
+ :title="`${runId} — click to copy`"
93
+ @click="copyRunId"
94
+ >
95
+ {{ runId }}
96
+ </p>
97
+ </div>
98
+
99
+ <!-- The model-activity rollup, embedded inline. The "View all calls →" link opens the
100
+ run's observability panel even when this step recorded no calls (e.g. a gate that
101
+ passed its precheck with no helper spun up), so every window reaches it the same
102
+ way; the metrics bar shows only when the step itself made calls. -->
103
+ <StepModelActivity :metrics="step.metrics" :instance-id="instanceId" />
104
+ </div>
105
+ </template>
@@ -12,6 +12,7 @@
12
12
  // `spec/features/*.feature` files would need a spec endpoint (a future enhancement).
13
13
  import type { TestConcern, TestOutcome, TestReport } from '~/types/domain'
14
14
  import StepRestartControl from '~/components/panels/StepRestartControl.vue'
15
+ import StepRunMeta from '~/components/panels/StepRunMeta.vue'
15
16
 
16
17
  const board = useBoardStore()
17
18
  const execution = useExecutionStore()
@@ -21,9 +22,12 @@ const execution = useExecutionStore()
21
22
  const { open, blockId, instanceId, stepIndex, close } = useResultView('tester')
22
23
  const block = computed(() => (blockId.value ? board.getBlock(blockId.value) : undefined))
23
24
 
25
+ const instance = computed(() =>
26
+ instanceId.value === null ? null : (execution.getInstance(instanceId.value) ?? null),
27
+ )
24
28
  const step = computed(() => {
25
- if (instanceId.value === null || stepIndex.value === null) return null
26
- return execution.getInstance(instanceId.value)?.steps[stepIndex.value] ?? null
29
+ if (instance.value === null || stepIndex.value === null) return null
30
+ return instance.value.steps[stepIndex.value] ?? null
27
31
  })
28
32
  const report = computed<TestReport | null>(() => step.value?.test?.lastReport ?? null)
29
33
  const testState = computed(() => step.value?.test ?? null)
@@ -385,12 +389,17 @@ const GROUP_STATUS_META: Record<ScenarioGroup['status'], { icon: string; text: s
385
389
  <p class="text-[12px] capitalize text-slate-300">{{ report.environment }}</p>
386
390
  </div>
387
391
 
388
- <div v-if="step?.model">
389
- <h4 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
390
- Model
391
- </h4>
392
- <p class="break-all text-[12px] text-slate-300">{{ step.model }}</p>
393
- </div>
392
+ <!-- Shared run metadata + embedded observability (model, run id, timing,
393
+ model-activity rollup) identical to the gate and agent step detail. -->
394
+ <StepRunMeta
395
+ v-if="step"
396
+ :step="step"
397
+ :instance-id="instanceId ?? undefined"
398
+ :step-number="stepIndex === null ? undefined : stepIndex + 1"
399
+ :total-steps="instance?.steps.length"
400
+ :run-failed="instance?.status === 'failed'"
401
+ :failure-at="instance?.failure?.occurredAt"
402
+ />
394
403
 
395
404
  <p class="mt-auto text-[10px] leading-relaxed text-slate-600">
396
405
  Scenarios are the areas the Tester chose to exercise (its spec acceptance scenarios).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cat-factory/app",
3
- "version": "0.7.2",
3
+ "version": "0.7.4",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/kibertoad/cat-factory.git",