@cat-factory/app 0.6.0 → 0.7.2

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.
@@ -12,11 +12,13 @@ import StepRestartControl from '~/components/panels/StepRestartControl.vue'
12
12
 
13
13
  const board = useBoardStore()
14
14
  const execution = useExecutionStore()
15
+ const ui = useUiStore()
15
16
 
16
17
  // Synchronous window: it reads its state straight off the execution step, so there's
17
18
  // nothing to fetch on open (no `onOpen` loader).
18
19
  const { open, blockId, instanceId, stepIndex, close } = useResultView('gate')
19
20
  const block = computed(() => (blockId.value ? board.getBlock(blockId.value) : undefined))
21
+ const prUrl = computed(() => block.value?.pullRequest?.url ?? null)
20
22
 
21
23
  const instance = computed(() =>
22
24
  instanceId.value === null ? null : (execution.getInstance(instanceId.value) ?? null),
@@ -35,6 +37,22 @@ const helperMeta = computed(() => agentKindMeta(helperKind.value))
35
37
  const failingChecks = computed(() => gate.value?.failingChecks ?? [])
36
38
  const shortSha = computed(() => (gate.value?.headSha ? gate.value.headSha.slice(0, 7) : null))
37
39
 
40
+ // The helper-agent attempts this gate dispatched, newest first for the timeline.
41
+ const attempts = computed(() => [...(gate.value?.attemptLog ?? [])].reverse())
42
+
43
+ function formatClock(ms?: number | null): string | null {
44
+ return ms ? new Date(ms).toLocaleString() : null
45
+ }
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
+
38
56
  /**
39
57
  * The display status — a roll-up of the persisted gate state + the run's status, so the
40
58
  * window reads as a conclusion rather than raw fields:
@@ -189,7 +207,21 @@ const conflictVerdict = computed(() => {
189
207
  class="flex items-center gap-2 rounded-md border border-slate-800 bg-slate-950/40 px-3 py-1.5"
190
208
  >
191
209
  <UIcon name="i-lucide-circle-x" class="h-3.5 w-3.5 shrink-0 text-rose-400" />
192
- <span class="min-w-0 flex-1 truncate text-[13px] text-slate-200">{{
210
+ <a
211
+ v-if="c.url"
212
+ :href="c.url"
213
+ target="_blank"
214
+ rel="noopener"
215
+ class="group min-w-0 flex-1 truncate text-[13px] text-sky-300 hover:text-sky-200 hover:underline"
216
+ :title="`Open ${c.name} on GitHub`"
217
+ >
218
+ {{ c.name }}
219
+ <UIcon
220
+ name="i-lucide-external-link"
221
+ class="ml-0.5 inline h-3 w-3 opacity-60 group-hover:opacity-100"
222
+ />
223
+ </a>
224
+ <span v-else class="min-w-0 flex-1 truncate text-[13px] text-slate-200">{{
193
225
  c.name
194
226
  }}</span>
195
227
  <span class="shrink-0 text-[11px] uppercase text-rose-300">
@@ -202,7 +234,7 @@ const conflictVerdict = computed(() => {
202
234
  </p>
203
235
  </template>
204
236
 
205
- <!-- Conflicts: verdict + note (no file-level detail from GitHub) -->
237
+ <!-- Conflicts: verdict + the resolver's account of what it left -->
206
238
  <template v-else>
207
239
  <h3 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
208
240
  Mergeability
@@ -217,11 +249,64 @@ const conflictVerdict = computed(() => {
217
249
  />
218
250
  <span class="text-[13px] text-slate-200">{{ conflictVerdict }}</span>
219
251
  </div>
252
+ <!-- GitHub's API reports mergeability as a single bit (no file list), but the
253
+ conflict resolver discovers the conflicting files in the container and
254
+ reports them back — surface that account here. -->
255
+ <p
256
+ v-if="gate.lastFailureSummary"
257
+ class="mt-2 whitespace-pre-wrap rounded-md border border-slate-800 bg-slate-950/40 px-3 py-2 text-[12px] leading-relaxed text-slate-300"
258
+ >
259
+ {{ gate.lastFailureSummary }}
260
+ </p>
220
261
  <p class="mt-2 text-[11px] leading-relaxed text-slate-500">
221
- GitHub reports mergeability as a single verdict, so there's no file-level conflict
222
- list here. The conflict resolver inspects the branch directly.
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
+ >
223
273
  </p>
224
274
  </template>
275
+
276
+ <!-- Attempt history (both gates): what each helper run did and how it ended. -->
277
+ <section v-if="attempts.length" class="mt-5">
278
+ <h3 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
279
+ {{ helperMeta.label }} attempts
280
+ </h3>
281
+ <ol class="space-y-2">
282
+ <li
283
+ v-for="a in attempts"
284
+ :key="a.attempt"
285
+ class="rounded-md border border-slate-800 bg-slate-950/40 px-3 py-2"
286
+ >
287
+ <div class="flex items-center gap-2">
288
+ <span class="text-[12px] font-semibold text-slate-200"
289
+ >Attempt {{ a.attempt }}</span
290
+ >
291
+ <UBadge
292
+ :color="a.outcome === 'failed' ? 'error' : 'neutral'"
293
+ variant="subtle"
294
+ size="sm"
295
+ >{{ a.outcome }}</UBadge
296
+ >
297
+ <span v-if="formatClock(a.at)" class="ml-auto text-[11px] text-slate-500">{{
298
+ formatClock(a.at)
299
+ }}</span>
300
+ </div>
301
+ <p
302
+ v-if="a.summary"
303
+ class="mt-1 whitespace-pre-wrap text-[12px] leading-relaxed text-slate-400"
304
+ >
305
+ {{ a.summary }}
306
+ </p>
307
+ </li>
308
+ </ol>
309
+ </section>
225
310
  </template>
226
311
  </div>
227
312
 
@@ -270,6 +355,26 @@ const conflictVerdict = computed(() => {
270
355
  <p class="break-all text-[12px] text-slate-300">{{ step.model }}</p>
271
356
  </div>
272
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>
377
+
273
378
  <p class="mt-auto text-[10px] leading-relaxed text-slate-600">
274
379
  A gate runs a programmatic precheck and only spins up the
275
380
  {{ helperMeta.label }} when it fails — a green check advances with nothing spun up.
@@ -262,6 +262,17 @@ watch(
262
262
  >
263
263
  Workspace settings
264
264
  </UButton>
265
+ <UButton
266
+ block
267
+ color="primary"
268
+ variant="soft"
269
+ size="sm"
270
+ icon="i-lucide-message-square-reply"
271
+ class="justify-start"
272
+ @click="ui.openIssueWriteback()"
273
+ >
274
+ Issue tracker writeback
275
+ </UButton>
265
276
  <UButton
266
277
  block
267
278
  color="primary"
@@ -1,102 +1,102 @@
1
- <script setup lang="ts">
2
- import { computed } from 'vue'
3
- import type { StepMetrics } from '~/types/execution'
4
- import {
5
- formatMs,
6
- formatTokens,
7
- headroomColor,
8
- headroomRatio,
9
- pct,
10
- transportRatio,
11
- } from '~/utils/observability'
12
-
13
- // Compact, at-a-glance LLM rollup for one pipeline step: token usage, an
14
- // output-limit headroom bar (how close the step ran to truncation), a
15
- // transport-vs-execution latency split, and error/warning badges. Rendered inline
16
- // on the step surfaces (step detail, pipeline timeline). A no-op when there are no
17
- // recorded calls. Clicking anywhere emits `inspect` so a parent can open the
18
- // drill-down panel.
19
- const props = defineProps<{ metrics: StepMetrics; clickable?: boolean }>()
20
- defineEmits<{ inspect: [] }>()
21
-
22
- const m = computed(() => props.metrics)
23
- const headroom = computed(() => headroomRatio(m.value))
24
- const transport = computed(() => transportRatio(m.value))
25
- const headroomTone = computed(() => headroomColor(headroom.value, m.value.truncatedCalls > 0))
26
- </script>
27
-
28
- <template>
29
- <div
30
- v-if="m.calls > 0"
31
- class="rounded-lg border border-slate-800 bg-slate-900/40 p-2.5 text-[12px]"
32
- :class="
33
- clickable ? 'cursor-pointer transition hover:border-slate-700 hover:bg-slate-900/70' : ''
34
- "
35
- :role="clickable ? 'button' : undefined"
36
- @click="clickable ? $emit('inspect') : undefined"
37
- >
38
- <!-- header line: call count + tokens + warning/error badges -->
39
- <div class="flex items-center gap-2">
40
- <UIcon name="i-lucide-activity" class="h-3.5 w-3.5 shrink-0 text-slate-500" />
41
- <span class="text-slate-300"> {{ m.calls }} {{ m.calls === 1 ? 'call' : 'calls' }} </span>
42
- <span class="text-slate-500">·</span>
43
- <span class="tabular-nums text-slate-400" title="Prompt / completion tokens">
44
- {{ formatTokens(m.promptTokens) }}↑ {{ formatTokens(m.completionTokens) }}↓
45
- </span>
46
- <div class="ml-auto flex items-center gap-1">
47
- <UBadge v-if="m.errors > 0" color="error" variant="subtle" size="sm">
48
- {{ m.errors }} error{{ m.errors === 1 ? '' : 's' }}
49
- </UBadge>
50
- <UBadge v-if="m.warnings > 0" color="warning" variant="subtle" size="sm">
51
- {{ m.warnings }} warning{{ m.warnings === 1 ? '' : 's' }}
52
- </UBadge>
53
- <UIcon v-if="clickable" name="i-lucide-chevron-right" class="h-3.5 w-3.5 text-slate-600" />
54
- </div>
55
- </div>
56
-
57
- <!-- output-limit headroom -->
58
- <div v-if="headroom !== null" class="mt-2">
59
- <div class="flex items-center justify-between text-[11px]">
60
- <span class="text-slate-500">Output limit</span>
61
- <span class="tabular-nums" :class="headroomTone">
62
- {{ formatTokens(m.peakCompletionTokens) }} /
63
- {{ formatTokens(m.maxOutputTokens ?? 0) }} ({{ pct(headroom) }}%)
64
- </span>
65
- </div>
66
- <div class="mt-1 h-1 overflow-hidden rounded-full bg-slate-700/60">
67
- <div
68
- class="h-full rounded-full transition-all duration-500"
69
- :class="
70
- m.truncatedCalls > 0 || headroom >= 0.98
71
- ? 'bg-rose-400'
72
- : headroom >= 0.8
73
- ? 'bg-amber-400'
74
- : 'bg-emerald-400'
75
- "
76
- :style="{ width: `${Math.max(2, pct(headroom))}%` }"
77
- />
78
- </div>
79
- <p v-if="m.truncatedCalls > 0" class="mt-1 text-[11px] text-rose-400">
80
- {{ m.truncatedCalls }} call{{ m.truncatedCalls === 1 ? '' : 's' }} truncated at the limit
81
- </p>
82
- </div>
83
-
84
- <!-- transport overhead vs model execution -->
85
- <div v-if="transport !== null" class="mt-2">
86
- <div class="flex items-center justify-between text-[11px]">
87
- <span class="text-slate-500">Transport vs execution</span>
88
- <span class="tabular-nums text-slate-400">
89
- {{ formatMs(m.overheadMs) }} / {{ formatMs(m.upstreamMs) }}
90
- </span>
91
- </div>
92
- <div class="mt-1 flex h-1 overflow-hidden rounded-full bg-slate-700/60">
93
- <div
94
- class="h-full bg-sky-400/80"
95
- :style="{ width: `${pct(transport)}%` }"
96
- title="Transport / proxy overhead"
97
- />
98
- <div class="h-full bg-indigo-400/80 flex-1" title="Model execution" />
99
- </div>
100
- </div>
101
- </div>
102
- </template>
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import type { StepMetrics } from '~/types/execution'
4
+ import {
5
+ formatMs,
6
+ formatTokens,
7
+ headroomColor,
8
+ headroomRatio,
9
+ pct,
10
+ transportRatio,
11
+ } from '~/utils/observability'
12
+
13
+ // Compact, at-a-glance LLM rollup for one pipeline step: token usage, an
14
+ // output-limit headroom bar (how close the step ran to truncation), a
15
+ // transport-vs-execution latency split, and error/warning badges. Rendered inline
16
+ // on the step surfaces (step detail, pipeline timeline). A no-op when there are no
17
+ // recorded calls. Clicking anywhere emits `inspect` so a parent can open the
18
+ // drill-down panel.
19
+ const props = defineProps<{ metrics: StepMetrics; clickable?: boolean }>()
20
+ defineEmits<{ inspect: [] }>()
21
+
22
+ const m = computed(() => props.metrics)
23
+ const headroom = computed(() => headroomRatio(m.value))
24
+ const transport = computed(() => transportRatio(m.value))
25
+ const headroomTone = computed(() => headroomColor(headroom.value, m.value.truncatedCalls > 0))
26
+ </script>
27
+
28
+ <template>
29
+ <div
30
+ v-if="m.calls > 0"
31
+ class="rounded-lg border border-slate-800 bg-slate-900/40 p-2.5 text-[12px]"
32
+ :class="
33
+ clickable ? 'cursor-pointer transition hover:border-slate-700 hover:bg-slate-900/70' : ''
34
+ "
35
+ :role="clickable ? 'button' : undefined"
36
+ @click="clickable ? $emit('inspect') : undefined"
37
+ >
38
+ <!-- header line: call count + tokens + warning/error badges -->
39
+ <div class="flex items-center gap-2">
40
+ <UIcon name="i-lucide-activity" class="h-3.5 w-3.5 shrink-0 text-slate-500" />
41
+ <span class="text-slate-300"> {{ m.calls }} {{ m.calls === 1 ? 'call' : 'calls' }} </span>
42
+ <span class="text-slate-500">·</span>
43
+ <span class="tabular-nums text-slate-400" title="Prompt / completion tokens">
44
+ {{ formatTokens(m.promptTokens) }}↑ {{ formatTokens(m.completionTokens) }}↓
45
+ </span>
46
+ <div class="ml-auto flex items-center gap-1">
47
+ <UBadge v-if="m.errors > 0" color="error" variant="subtle" size="sm">
48
+ {{ m.errors }} error{{ m.errors === 1 ? '' : 's' }}
49
+ </UBadge>
50
+ <UBadge v-if="m.warnings > 0" color="warning" variant="subtle" size="sm">
51
+ {{ m.warnings }} warning{{ m.warnings === 1 ? '' : 's' }}
52
+ </UBadge>
53
+ <UIcon v-if="clickable" name="i-lucide-chevron-right" class="h-3.5 w-3.5 text-slate-600" />
54
+ </div>
55
+ </div>
56
+
57
+ <!-- output-limit headroom -->
58
+ <div v-if="headroom !== null" class="mt-2">
59
+ <div class="flex items-center justify-between text-[11px]">
60
+ <span class="text-slate-500">Output limit</span>
61
+ <span class="tabular-nums" :class="headroomTone">
62
+ {{ formatTokens(m.peakCompletionTokens) }} /
63
+ {{ formatTokens(m.maxOutputTokens ?? 0) }} ({{ pct(headroom) }}%)
64
+ </span>
65
+ </div>
66
+ <div class="mt-1 h-1 overflow-hidden rounded-full bg-slate-700/60">
67
+ <div
68
+ class="h-full rounded-full transition-all duration-500"
69
+ :class="
70
+ m.truncatedCalls > 0 || headroom >= 0.98
71
+ ? 'bg-rose-400'
72
+ : headroom >= 0.8
73
+ ? 'bg-amber-400'
74
+ : 'bg-emerald-400'
75
+ "
76
+ :style="{ width: `${Math.max(2, pct(headroom))}%` }"
77
+ />
78
+ </div>
79
+ <p v-if="m.truncatedCalls > 0" class="mt-1 text-[11px] text-rose-400">
80
+ {{ m.truncatedCalls }} call{{ m.truncatedCalls === 1 ? '' : 's' }} truncated at the limit
81
+ </p>
82
+ </div>
83
+
84
+ <!-- transport overhead vs model execution -->
85
+ <div v-if="transport !== null" class="mt-2">
86
+ <div class="flex items-center justify-between text-[11px]">
87
+ <span class="text-slate-500">Transport vs execution</span>
88
+ <span class="tabular-nums text-slate-400">
89
+ {{ formatMs(m.overheadMs) }} / {{ formatMs(m.upstreamMs) }}
90
+ </span>
91
+ </div>
92
+ <div class="mt-1 flex h-1 overflow-hidden rounded-full bg-slate-700/60">
93
+ <div
94
+ class="h-full bg-sky-400/80"
95
+ :style="{ width: `${pct(transport)}%` }"
96
+ title="Transport / proxy overhead"
97
+ />
98
+ <div class="h-full bg-indigo-400/80 flex-1" title="Model execution" />
99
+ </div>
100
+ </div>
101
+ </div>
102
+ </template>