@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.
- package/LICENSE +21 -21
- package/app/components/board/ContextPicker.vue +367 -367
- package/app/components/gates/GateResultView.vue +109 -4
- package/app/components/layout/SideBar.vue +11 -0
- package/app/components/observability/StepMetricsBar.vue +102 -102
- package/app/components/panels/inspector/RecurringScheduleSettings.vue +178 -178
- package/app/components/panels/inspector/TaskRunSettings.vue +77 -0
- package/app/components/recurring/RecurrenceEditor.vue +124 -124
- package/app/components/settings/IssueTrackerWritebackPanel.vue +103 -0
- package/app/composables/useBlockQueries.ts +154 -154
- package/app/composables/useContextLinking.ts +65 -65
- package/app/composables/useFrameResize.ts +54 -54
- package/app/pages/index.vue +2 -0
- package/app/stores/documents.ts +176 -176
- package/app/stores/services.ts +87 -87
- package/app/stores/tracker.ts +39 -27
- package/app/stores/ui.ts +12 -0
- package/app/types/documents.ts +104 -104
- package/app/types/domain.ts +5 -1
- package/app/types/execution.ts +18 -0
- package/app/types/github.ts +173 -173
- package/app/types/services.ts +27 -27
- package/app/types/tasks.ts +82 -82
- package/app/types/tracker.ts +27 -18
- package/app/utils/agentOutput.spec.ts +128 -128
- package/app/utils/agentOutput.ts +173 -173
- package/app/utils/observability.ts +52 -52
- package/package.json +6 -1
|
@@ -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
|
-
<
|
|
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 +
|
|
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
|
|
222
|
-
|
|
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>
|