@cat-factory/app 0.195.1 → 0.196.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.
- package/app/components/board/nodes/BlockNode.vue +23 -4
- package/app/components/judge/JudgeResultView.vue +5 -1
- package/app/components/merge/MergeEffortChips.vue +5 -1
- package/app/components/panels/ReportsPanel.vue +5 -2
- package/app/components/panels/ReportsSpendBreakdown.vue +5 -1
- package/app/components/panels/StepEffortReport.vue +11 -2
- package/app/components/panels/StepFragmentAdherence.vue +7 -2
- package/app/components/panels/StepMetadataCard.vue +20 -1
- package/app/components/panels/StepRunMeta.vue +18 -0
- package/app/components/pipeline/EstimateThresholdFields.vue +74 -0
- package/app/components/pipeline/OutputBudgetInput.vue +1 -0
- package/app/components/pipeline/PipelineBuilder.vue +94 -96
- package/app/components/settings/ConsensusGroupsSection.vue +12 -3
- package/app/composables/api/errors.ts +7 -0
- package/app/composables/usePipelineErrorToast.spec.ts +119 -5
- package/app/composables/usePipelineErrorToast.ts +140 -10
- package/app/composables/useStepPromptVariant.spec.ts +75 -0
- package/app/composables/useStepPromptVariant.ts +50 -0
- package/app/stores/agents.spec.ts +30 -0
- package/app/stores/agents.ts +33 -1
- package/app/stores/pipelines/draftStepConfig.ts +24 -1
- package/app/stores/workspace/hydrate.ts +3 -0
- package/app/types/domain.ts +1 -0
- package/app/utils/estimateGating.spec.ts +44 -0
- package/app/utils/estimateGating.ts +60 -0
- package/i18n/locales/de.json +49 -4
- package/i18n/locales/en.json +58 -4
- package/i18n/locales/es.json +49 -4
- package/i18n/locales/fr.json +49 -4
- package/i18n/locales/he.json +49 -4
- package/i18n/locales/it.json +49 -4
- package/i18n/locales/ja.json +49 -4
- package/i18n/locales/pl.json +49 -4
- package/i18n/locales/tr.json +49 -4
- package/i18n/locales/uk.json +49 -4
- package/package.json +2 -2
|
@@ -75,6 +75,21 @@ const FRAME_LABEL_KEYS: Record<BlockStatus, string> = {
|
|
|
75
75
|
done: 'board.frame.status.done',
|
|
76
76
|
}
|
|
77
77
|
const statusLabel = computed(() => t(FRAME_LABEL_KEYS[frameStatus.value]))
|
|
78
|
+
// The badge is a ROLLUP over the frame's children, not a status anybody set on the service, and
|
|
79
|
+
// the label alone never says which child produced it — "Needs attention" names no task, and
|
|
80
|
+
// "Live" reads as a health check rather than "caught up". So each status gets its own tooltip
|
|
81
|
+
// naming what it is rolled up from. `pr_ready`/`done` are unreachable here (`frameStatus` caps
|
|
82
|
+
// below `done`: a service is long-lived and never finishes) but the Record stays exhaustive over
|
|
83
|
+
// `BlockStatus` for the same reason the label map does, and mirrors that map's aliasing.
|
|
84
|
+
const FRAME_STATUS_HINT_KEYS: Record<BlockStatus, string> = {
|
|
85
|
+
planned: 'board.frame.statusHint.planned',
|
|
86
|
+
ready: 'board.frame.statusHint.ready',
|
|
87
|
+
in_progress: 'board.frame.statusHint.in_progress',
|
|
88
|
+
blocked: 'board.frame.statusHint.blocked',
|
|
89
|
+
pr_ready: 'board.frame.statusHint.pr_ready',
|
|
90
|
+
done: 'board.frame.statusHint.done',
|
|
91
|
+
}
|
|
92
|
+
const statusHint = computed(() => t(FRAME_STATUS_HINT_KEYS[frameStatus.value]))
|
|
78
93
|
|
|
79
94
|
const selected = computed(() => ui.selectedBlockId === props.id)
|
|
80
95
|
// Services are always expanded to their task canvas, at every zoom level: there is no
|
|
@@ -349,7 +364,7 @@ const ITEM_ICON: Record<string, string> = {
|
|
|
349
364
|
</UBadge>
|
|
350
365
|
</div>
|
|
351
366
|
<div class="flex items-center justify-between">
|
|
352
|
-
<UBadge :color="statusMeta.chip as any" variant="subtle" size="sm">{{
|
|
367
|
+
<UBadge :color="statusMeta.chip as any" variant="subtle" size="sm" :title="statusHint">{{
|
|
353
368
|
statusLabel
|
|
354
369
|
}}</UBadge>
|
|
355
370
|
<span class="text-[11px] text-slate-400">{{
|
|
@@ -474,9 +489,13 @@ const ITEM_ICON: Record<string, string> = {
|
|
|
474
489
|
</div>
|
|
475
490
|
</div>
|
|
476
491
|
<div class="flex items-center gap-1">
|
|
477
|
-
<UBadge
|
|
478
|
-
|
|
479
|
-
|
|
492
|
+
<UBadge
|
|
493
|
+
:color="statusMeta.chip as any"
|
|
494
|
+
variant="subtle"
|
|
495
|
+
size="sm"
|
|
496
|
+
:title="statusHint"
|
|
497
|
+
>{{ statusLabel }}</UBadge
|
|
498
|
+
>
|
|
480
499
|
<!-- Board-authoring buttons (create task / from issue / recurring / initiative)
|
|
481
500
|
are `board.write` — hidden for a read-only viewer, who keeps the badge +
|
|
482
501
|
collapse toggle (view-only affordances). -->
|
|
@@ -151,7 +151,11 @@ async function act(choice: 'proceed' | 'bounce' | 'stop') {
|
|
|
151
151
|
<span class="text-[13px] font-semibold text-slate-100">
|
|
152
152
|
{{ score === null ? t('judge.notScored') : n(score, 'percent') }}
|
|
153
153
|
</span>
|
|
154
|
-
<span
|
|
154
|
+
<span
|
|
155
|
+
v-if="threshold !== null"
|
|
156
|
+
class="text-[12px] text-slate-400"
|
|
157
|
+
:title="t('judge.thresholdHint')"
|
|
158
|
+
>
|
|
155
159
|
{{ t('judge.threshold', { threshold: n(threshold, 'percent') }) }}
|
|
156
160
|
</span>
|
|
157
161
|
<span v-if="judge.disposition" class="text-[12px] text-slate-400">
|
|
@@ -87,7 +87,10 @@ function pick(effort: ReviewEffort) {
|
|
|
87
87
|
<template>
|
|
88
88
|
<div data-testid="merge-effort-chips" class="mt-2">
|
|
89
89
|
<div class="flex items-center gap-1.5">
|
|
90
|
-
<span
|
|
90
|
+
<span
|
|
91
|
+
class="text-[10px] uppercase tracking-wide text-slate-500"
|
|
92
|
+
:title="t('merge.effort.promptHint')"
|
|
93
|
+
>
|
|
91
94
|
{{ t('merge.effort.prompt') }}
|
|
92
95
|
</span>
|
|
93
96
|
<UBadge
|
|
@@ -96,6 +99,7 @@ function pick(effort: ReviewEffort) {
|
|
|
96
99
|
color="neutral"
|
|
97
100
|
variant="subtle"
|
|
98
101
|
size="sm"
|
|
102
|
+
:title="t('merge.changeClass.hint')"
|
|
99
103
|
>
|
|
100
104
|
{{ classLabel }}
|
|
101
105
|
</UBadge>
|
|
@@ -309,12 +309,15 @@ watch(
|
|
|
309
309
|
</div>
|
|
310
310
|
</div>
|
|
311
311
|
<div class="mt-2 flex items-center gap-4 text-[11px] text-slate-500">
|
|
312
|
-
<span class="flex items-center gap-1">
|
|
312
|
+
<span class="flex items-center gap-1" :title="t('reports.legend.meteredHint')">
|
|
313
313
|
<span class="h-2 w-2 rounded-sm bg-violet-500" />{{
|
|
314
314
|
t('reports.legend.metered')
|
|
315
315
|
}}
|
|
316
316
|
</span>
|
|
317
|
-
<span
|
|
317
|
+
<span
|
|
318
|
+
class="flex items-center gap-1"
|
|
319
|
+
:title="t('reports.legend.subscriptionHint')"
|
|
320
|
+
>
|
|
318
321
|
<span class="h-2 w-2 rounded-sm bg-amber-600" />{{
|
|
319
322
|
t('reports.legend.subscription')
|
|
320
323
|
}}
|
|
@@ -40,7 +40,11 @@ const max = computed(() => maxOf(props.rows, spendMagnitude))
|
|
|
40
40
|
figure would report money that was never billed. -->
|
|
41
41
|
<span class="shrink-0 tabular-nums text-slate-400">
|
|
42
42
|
{{ money(row.meteredCost) }}
|
|
43
|
-
<span
|
|
43
|
+
<span
|
|
44
|
+
v-if="row.subscriptionCost > 0"
|
|
45
|
+
class="text-amber-400"
|
|
46
|
+
:title="t('reports.legend.subscriptionHint')"
|
|
47
|
+
>
|
|
44
48
|
{{ t('reports.spend.subscriptionAside', { value: money(row.subscriptionCost) }) }}
|
|
45
49
|
</span>
|
|
46
50
|
</span>
|
|
@@ -42,7 +42,12 @@ const difficultyClass = computed(() => BAR_CLASS[effortBand(props.report.difficu
|
|
|
42
42
|
</div>
|
|
43
43
|
|
|
44
44
|
<div class="flex items-center gap-2">
|
|
45
|
-
<span
|
|
45
|
+
<span
|
|
46
|
+
class="text-[12px] text-slate-300"
|
|
47
|
+
:title="t('panels.stepDetail.effort.difficultyHint')"
|
|
48
|
+
>
|
|
49
|
+
{{ t('panels.stepDetail.effort.difficulty') }}
|
|
50
|
+
</span>
|
|
46
51
|
<div class="h-1.5 flex-1 overflow-hidden rounded-full bg-slate-700/60">
|
|
47
52
|
<div
|
|
48
53
|
class="h-full rounded-full"
|
|
@@ -50,7 +55,11 @@ const difficultyClass = computed(() => BAR_CLASS[effortBand(props.report.difficu
|
|
|
50
55
|
:style="{ width: `${difficultyPct}%` }"
|
|
51
56
|
/>
|
|
52
57
|
</div>
|
|
53
|
-
<span
|
|
58
|
+
<span
|
|
59
|
+
data-testid="step-effort-difficulty"
|
|
60
|
+
class="text-[12px] font-medium text-slate-200"
|
|
61
|
+
:title="t('panels.stepDetail.effort.difficultyHint')"
|
|
62
|
+
>
|
|
54
63
|
{{ t('panels.stepDetail.effort.outOfTen', { value: report.difficulty }) }}
|
|
55
64
|
</span>
|
|
56
65
|
</div>
|
|
@@ -33,7 +33,9 @@ function label(item: FragmentAdherence[number]): string {
|
|
|
33
33
|
class="mb-2 flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wide text-slate-400"
|
|
34
34
|
>
|
|
35
35
|
<UIcon name="i-lucide-clipboard-check" class="h-3.5 w-3.5" />
|
|
36
|
-
<span
|
|
36
|
+
<span :title="t('panels.stepDetail.adherence.headingHint')">
|
|
37
|
+
{{ t('panels.stepDetail.adherence.heading') }}
|
|
38
|
+
</span>
|
|
37
39
|
</div>
|
|
38
40
|
|
|
39
41
|
<div class="space-y-2.5">
|
|
@@ -54,7 +56,10 @@ function label(item: FragmentAdherence[number]): string {
|
|
|
54
56
|
:style="{ width: `${ratingPct(item.rating)}%` }"
|
|
55
57
|
/>
|
|
56
58
|
</div>
|
|
57
|
-
<span
|
|
59
|
+
<span
|
|
60
|
+
class="shrink-0 text-[12px] font-medium text-slate-200"
|
|
61
|
+
:title="t('panels.stepDetail.adherence.ratingHint')"
|
|
62
|
+
>
|
|
58
63
|
{{ t('panels.stepDetail.adherence.outOfTen', { value: item.rating }) }}
|
|
59
64
|
</span>
|
|
60
65
|
</div>
|
|
@@ -48,6 +48,13 @@ const stateMeta = computed(() => {
|
|
|
48
48
|
|
|
49
49
|
const modelLabel = computed(() => (props.step.model ? models.labelForRef(props.step.model) : null))
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* The deployment-registered VARIANT this step ran under — an alternate prompt for its agent kind.
|
|
53
|
+
* Reported beside the model because it is the other half of "what actually ran"; null on every
|
|
54
|
+
* step that ran the shipped prompt, so the field is simply absent on the stock product.
|
|
55
|
+
*/
|
|
56
|
+
const promptVariant = useStepPromptVariant(() => props.step)
|
|
57
|
+
|
|
51
58
|
const ITEM_ICON: Record<string, string> = {
|
|
52
59
|
completed: 'i-lucide-check-circle-2',
|
|
53
60
|
in_progress: 'i-lucide-loader-circle',
|
|
@@ -140,6 +147,15 @@ async function copyRunId() {
|
|
|
140
147
|
{{ modelLabel ?? t('panels.stepMeta.notRecorded') }}
|
|
141
148
|
</dd>
|
|
142
149
|
</div>
|
|
150
|
+
<div v-if="promptVariant">
|
|
151
|
+
<dt class="text-[11px] uppercase tracking-wide text-slate-500">
|
|
152
|
+
{{ t('panels.stepMeta.promptVariant') }}
|
|
153
|
+
</dt>
|
|
154
|
+
<dd class="mt-0.5 truncate text-slate-300">{{ promptVariant.label }}</dd>
|
|
155
|
+
<dd v-if="promptVariant.note" class="mt-0.5 text-[11px] text-amber-400/80">
|
|
156
|
+
{{ promptVariant.note }}
|
|
157
|
+
</dd>
|
|
158
|
+
</div>
|
|
143
159
|
<!-- The run id this step belongs to, surfaced for debugging (copyable). -->
|
|
144
160
|
<div class="col-span-2 sm:col-span-3">
|
|
145
161
|
<dt class="text-[11px] uppercase tracking-wide text-slate-500">
|
|
@@ -207,7 +223,10 @@ async function copyRunId() {
|
|
|
207
223
|
|
|
208
224
|
<!-- standards (prompt fragments) folded into this step -->
|
|
209
225
|
<div v-if="step.selectedFragmentIds && step.selectedFragmentIds.length" class="mt-4">
|
|
210
|
-
<div
|
|
226
|
+
<div
|
|
227
|
+
class="text-[11px] uppercase tracking-wide text-slate-500"
|
|
228
|
+
:title="t('panels.stepMeta.standardsAppliedHint')"
|
|
229
|
+
>
|
|
211
230
|
{{ t('panels.stepMeta.standardsApplied') }}
|
|
212
231
|
</div>
|
|
213
232
|
<div class="mt-1 flex flex-wrap gap-1">
|
|
@@ -27,6 +27,14 @@ const props = defineProps<{
|
|
|
27
27
|
const models = useModelsStore()
|
|
28
28
|
const { t, d } = useI18n()
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* The deployment-registered VARIANT this step ran under — an alternate prompt for its agent kind.
|
|
32
|
+
* Reported beside the model because it is the other half of "what actually ran": two steps of the
|
|
33
|
+
* same kind on the same model can be told to be different things, and nothing else on this panel
|
|
34
|
+
* would say so. Null on every step that ran the shipped prompt.
|
|
35
|
+
*/
|
|
36
|
+
const promptVariant = useStepPromptVariant(() => props.step)
|
|
37
|
+
|
|
30
38
|
const { isRunning, durationLabel, activityAgoLabel } = useStepTimer({
|
|
31
39
|
step: () => props.step,
|
|
32
40
|
runFailed: () => props.runFailed ?? false,
|
|
@@ -110,6 +118,16 @@ async function copyRunId() {
|
|
|
110
118
|
</p>
|
|
111
119
|
</div>
|
|
112
120
|
|
|
121
|
+
<div v-if="promptVariant">
|
|
122
|
+
<h4 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
|
123
|
+
{{ t('panels.stepMeta.promptVariant') }}
|
|
124
|
+
</h4>
|
|
125
|
+
<p class="break-all text-[12px] text-slate-300">{{ promptVariant.label }}</p>
|
|
126
|
+
<p v-if="promptVariant.note" class="mt-0.5 text-[11px] text-amber-400/80">
|
|
127
|
+
{{ promptVariant.note }}
|
|
128
|
+
</p>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
113
131
|
<div v-if="runId">
|
|
114
132
|
<h4 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
|
115
133
|
{{ t('panels.stepMeta.run') }}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// The estimate-gate threshold row, shared by every gate editor in the pipeline builder: a
|
|
3
|
+
// companion step's own gate, a consensus step's escalation gate, and the Tester QC companion's
|
|
4
|
+
// gate. One component because the three rendered the same 0..1 inputs from three copies of the
|
|
5
|
+
// markup — and, more to the point, because the explanation these numbers need has to be worded
|
|
6
|
+
// once. A bare "risk >=" label says nothing about where the score comes from, what its scale is,
|
|
7
|
+
// or how the axes combine, and the scale is the trap: every other score the product shows a user
|
|
8
|
+
// (judge verdicts, effort difficulty, adherence ratings) is out of ten, so an unlabelled 0..1
|
|
9
|
+
// field reads as "risk 1 out of 10" and gates a step that was meant to run on everything.
|
|
10
|
+
//
|
|
11
|
+
// The gate itself is edited by the parent (the builder mutates store drafts in place), so this
|
|
12
|
+
// component only renders and reports: it never writes through the `gating` prop.
|
|
13
|
+
import type { EstimateAxis } from '~/utils/estimateGating'
|
|
14
|
+
import {
|
|
15
|
+
ESTIMATE_AXIS_FIELD,
|
|
16
|
+
ESTIMATE_AXIS_HINT_KEYS,
|
|
17
|
+
ESTIMATE_AXIS_LABEL_KEYS,
|
|
18
|
+
parseAxisThreshold,
|
|
19
|
+
} from '~/utils/estimateGating'
|
|
20
|
+
|
|
21
|
+
const props = defineProps<{
|
|
22
|
+
/** The gate being edited. Read-only here — changes go back through `update`. */
|
|
23
|
+
gating: { minComplexity?: number; minRisk?: number; minImpact?: number }
|
|
24
|
+
/** Which axes this gate exposes, in render order. */
|
|
25
|
+
axes: readonly EstimateAxis[]
|
|
26
|
+
/** What clearing the gate DOES, which differs per gate and is half the explanation. */
|
|
27
|
+
outcome: 'step' | 'consensus'
|
|
28
|
+
}>()
|
|
29
|
+
|
|
30
|
+
const emit = defineEmits<{ update: [axis: EstimateAxis, value: number | undefined] }>()
|
|
31
|
+
|
|
32
|
+
const { t } = useI18n()
|
|
33
|
+
|
|
34
|
+
// Exhaustive Record over the two outcomes with LITERAL keys, so the typed-message-key check sees
|
|
35
|
+
// them and a third outcome fails the typecheck rather than falling back to the step wording.
|
|
36
|
+
const OUTCOME_HINT_KEYS: Record<'step' | 'consensus', string> = {
|
|
37
|
+
step: 'pipeline.builder.gatingRunWhenAnyHint',
|
|
38
|
+
consensus: 'pipeline.builder.gatingRunWhenAnyConsensusHint',
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const fields = computed(() =>
|
|
42
|
+
props.axes.map((axis) => ({
|
|
43
|
+
axis,
|
|
44
|
+
label: t(ESTIMATE_AXIS_LABEL_KEYS[axis]),
|
|
45
|
+
hint: t(ESTIMATE_AXIS_HINT_KEYS[axis]),
|
|
46
|
+
value: props.gating[ESTIMATE_AXIS_FIELD[axis]],
|
|
47
|
+
})),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
function commit(axis: EstimateAxis, raw: string) {
|
|
51
|
+
emit('update', axis, parseAxisThreshold(raw))
|
|
52
|
+
}
|
|
53
|
+
</script>
|
|
54
|
+
|
|
55
|
+
<template>
|
|
56
|
+
<div class="flex flex-wrap items-center gap-2 border-t border-slate-800 pt-2">
|
|
57
|
+
<span class="text-[10px] text-slate-500" :title="t(OUTCOME_HINT_KEYS[outcome])">
|
|
58
|
+
{{ t('pipeline.builder.runWhenAny') }}
|
|
59
|
+
</span>
|
|
60
|
+
<template v-for="f in fields" :key="f.axis">
|
|
61
|
+
<label class="text-slate-400" :title="f.hint">{{ f.label }}</label>
|
|
62
|
+
<input
|
|
63
|
+
:value="f.value"
|
|
64
|
+
:title="f.hint"
|
|
65
|
+
type="number"
|
|
66
|
+
min="0"
|
|
67
|
+
max="1"
|
|
68
|
+
step="0.1"
|
|
69
|
+
class="w-14 rounded border border-slate-700 bg-slate-900 px-1.5 py-0.5 text-slate-100"
|
|
70
|
+
@change="commit(f.axis, ($event.target as HTMLInputElement).value)"
|
|
71
|
+
/>
|
|
72
|
+
</template>
|
|
73
|
+
</div>
|
|
74
|
+
</template>
|
|
@@ -63,6 +63,7 @@ function commit(raw: string | number) {
|
|
|
63
63
|
:min="MIN_AGENT_MAX_OUTPUT_TOKENS"
|
|
64
64
|
:max="MAX_AGENT_MAX_OUTPUT_TOKENS"
|
|
65
65
|
:placeholder="placeholder"
|
|
66
|
+
:title="t('pipeline.outputBudget.hint')"
|
|
66
67
|
:disabled="disabled"
|
|
67
68
|
@change="commit(($event.target as HTMLInputElement).value)"
|
|
68
69
|
/>
|
|
@@ -5,7 +5,9 @@ import type { AgentKind, Pipeline, PipelinePurpose } from '~/types/domain'
|
|
|
5
5
|
import AgentPalette from '~/components/palettes/AgentPalette.vue'
|
|
6
6
|
import AgentKindIcon from '~/components/pipeline/AgentKindIcon.vue'
|
|
7
7
|
import AgentPromptEditor from '~/components/pipeline/AgentPromptEditor.vue'
|
|
8
|
+
import EstimateThresholdFields from '~/components/pipeline/EstimateThresholdFields.vue'
|
|
8
9
|
import OutputBudgetInput from '~/components/pipeline/OutputBudgetInput.vue'
|
|
10
|
+
import { ESTIMATE_AXES, ESTIMATE_AXIS_FIELD, type EstimateAxis } from '~/utils/estimateGating'
|
|
9
11
|
import { showOverrideField } from '~/utils/uiMode'
|
|
10
12
|
import {
|
|
11
13
|
agentKindMeta,
|
|
@@ -47,6 +49,31 @@ function addParticipant(i: number) {
|
|
|
47
49
|
function removeParticipant(i: number, pIdx: number) {
|
|
48
50
|
pipelines.draftConsensus[i]?.participants.splice(pIdx, 1)
|
|
49
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* The consensus escalation gate exposes only the two axes its toggle seeds (risk + impact) —
|
|
54
|
+
* complexity is a fine reason to run a deeper TEST audit but a poor reason to convene a panel,
|
|
55
|
+
* which is about disagreement risk. A module constant rather than a template literal so the
|
|
56
|
+
* array identity is stable across re-renders.
|
|
57
|
+
*/
|
|
58
|
+
const CONSENSUS_ESTIMATE_AXES: readonly EstimateAxis[] = ['risk', 'impact']
|
|
59
|
+
|
|
60
|
+
// Writing one axis floor back onto each of the three draft gates. These are functions rather
|
|
61
|
+
// than assignments inlined in the template because a template `v-if` does not narrow inside an
|
|
62
|
+
// event handler's closure, so the inline form needs a non-null assertion at every call site —
|
|
63
|
+
// and a `!` on a gate that has since been toggled off is the one shape that throws.
|
|
64
|
+
function setCompanionGatingAxis(i: number | null, axis: EstimateAxis, value: number | undefined) {
|
|
65
|
+
const gating = i === null ? undefined : pipelines.draftGating[i]
|
|
66
|
+
if (gating) gating[ESTIMATE_AXIS_FIELD[axis]] = value
|
|
67
|
+
}
|
|
68
|
+
function setConsensusGatingAxis(i: number, axis: EstimateAxis, value: number | undefined) {
|
|
69
|
+
const gating = pipelines.draftConsensus[i]?.gating
|
|
70
|
+
if (gating) gating[ESTIMATE_AXIS_FIELD[axis]] = value
|
|
71
|
+
}
|
|
72
|
+
function setTesterQualityGatingAxis(i: number, axis: EstimateAxis, value: number | undefined) {
|
|
73
|
+
const gating = pipelines.draftTesterQuality[i]?.gating
|
|
74
|
+
if (gating) gating[ESTIMATE_AXIS_FIELD[axis]] = value
|
|
75
|
+
}
|
|
76
|
+
|
|
50
77
|
/** Toggle gating on/off for a draft step's consensus config. */
|
|
51
78
|
function toggleGating(i: number) {
|
|
52
79
|
const cfg = pipelines.draftConsensus[i]
|
|
@@ -124,6 +151,29 @@ function showOutputBudget(index: number): boolean {
|
|
|
124
151
|
function inheritedOutputBudget(kind: AgentKind): number | undefined {
|
|
125
152
|
return agentSettings.maxOutputTokensFor(kind)
|
|
126
153
|
}
|
|
154
|
+
/**
|
|
155
|
+
* Whether to offer the agent-kind VARIANT picker on this step: only when the deployment
|
|
156
|
+
* registered variants for its kind, and then only in advanced mode WHILE the step is still on the
|
|
157
|
+
* shipped prompt. Picking a variant is an OVERRIDE of what the kind ships, so `showOverrideField`
|
|
158
|
+
* keeps it visible the moment one is set — a step varied by a teammate (or by the API) must never
|
|
159
|
+
* become invisible to a basic-mode user who would then have no way to see, let alone undo, it.
|
|
160
|
+
*/
|
|
161
|
+
function showVariantPicker(index: number, kind: AgentKind): boolean {
|
|
162
|
+
if (!agents.variantsForKind(kind).length) return false
|
|
163
|
+
return showOverrideField(uiMode.isAdvanced, pipelines.draftAgentVariantId(index) ?? null)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* The variants registered for a step's kind as USelect items, with an explicit "shipped prompt"
|
|
168
|
+
* entry so clearing the pick is a choice in the same list rather than a separate affordance.
|
|
169
|
+
*/
|
|
170
|
+
function variantSelectItems(kind: AgentKind) {
|
|
171
|
+
return [
|
|
172
|
+
{ label: t('pipeline.builder.variantShipped'), value: '' },
|
|
173
|
+
...agents.variantsForKind(kind).map((variant) => ({ label: variant.label, value: variant.id })),
|
|
174
|
+
]
|
|
175
|
+
}
|
|
176
|
+
|
|
127
177
|
const releaseHealth = useReleaseHealthStore()
|
|
128
178
|
const skills = useSkillsStore()
|
|
129
179
|
|
|
@@ -719,6 +769,29 @@ async function clone(p: Pipeline) {
|
|
|
719
769
|
</p>
|
|
720
770
|
</div>
|
|
721
771
|
|
|
772
|
+
<!-- Agent-kind VARIANT picker: a deployment-registered alternate prompt for this
|
|
773
|
+
step's kind (`stepOptions.agentVariantId`). The step still runs the kind — only
|
|
774
|
+
the prompt changes — so this is an override of the shipped text, shown only where
|
|
775
|
+
the deployment registered one. -->
|
|
776
|
+
<div
|
|
777
|
+
v-if="showVariantPicker(unit.index, unit.kind)"
|
|
778
|
+
class="ms-6 flex items-center gap-2"
|
|
779
|
+
>
|
|
780
|
+
<span class="text-[10px] text-slate-500">
|
|
781
|
+
{{ t('pipeline.builder.variantLabel') }}
|
|
782
|
+
</span>
|
|
783
|
+
<USelect
|
|
784
|
+
class="w-56"
|
|
785
|
+
:model-value="pipelines.draftAgentVariantId(unit.index) ?? ''"
|
|
786
|
+
:items="variantSelectItems(unit.kind)"
|
|
787
|
+
value-key="value"
|
|
788
|
+
size="xs"
|
|
789
|
+
@update:model-value="
|
|
790
|
+
pipelines.setDraftAgentVariantId(unit.index, $event || undefined)
|
|
791
|
+
"
|
|
792
|
+
/>
|
|
793
|
+
</div>
|
|
794
|
+
|
|
722
795
|
<!-- This step's own output-token ceiling. An OVERRIDE of the workspace's per-kind
|
|
723
796
|
setting (itself an override of the deployment routing default), so it is
|
|
724
797
|
advanced-only until a value is pinned; empty inherits. -->
|
|
@@ -786,43 +859,15 @@ async function clone(p: Pipeline) {
|
|
|
786
859
|
@click="pipelines.toggleDraftGating(unit.companionIndex)"
|
|
787
860
|
/>
|
|
788
861
|
</div>
|
|
789
|
-
<
|
|
862
|
+
<EstimateThresholdFields
|
|
790
863
|
v-if="pipelines.draftGating[unit.companionIndex]?.enabled"
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
}}</label>
|
|
799
|
-
<input
|
|
800
|
-
v-model.number="pipelines.draftGating[unit.companionIndex]!.minComplexity"
|
|
801
|
-
type="number"
|
|
802
|
-
min="0"
|
|
803
|
-
max="1"
|
|
804
|
-
step="0.1"
|
|
805
|
-
class="w-14 rounded border border-slate-700 bg-slate-900 px-1.5 py-0.5 text-slate-100"
|
|
806
|
-
/>
|
|
807
|
-
<label class="text-slate-400">{{ t('pipeline.builder.riskThreshold') }}</label>
|
|
808
|
-
<input
|
|
809
|
-
v-model.number="pipelines.draftGating[unit.companionIndex]!.minRisk"
|
|
810
|
-
type="number"
|
|
811
|
-
min="0"
|
|
812
|
-
max="1"
|
|
813
|
-
step="0.1"
|
|
814
|
-
class="w-14 rounded border border-slate-700 bg-slate-900 px-1.5 py-0.5 text-slate-100"
|
|
815
|
-
/>
|
|
816
|
-
<label class="text-slate-400">{{ t('pipeline.builder.impactThreshold') }}</label>
|
|
817
|
-
<input
|
|
818
|
-
v-model.number="pipelines.draftGating[unit.companionIndex]!.minImpact"
|
|
819
|
-
type="number"
|
|
820
|
-
min="0"
|
|
821
|
-
max="1"
|
|
822
|
-
step="0.1"
|
|
823
|
-
class="w-14 rounded border border-slate-700 bg-slate-900 px-1.5 py-0.5 text-slate-100"
|
|
824
|
-
/>
|
|
825
|
-
</div>
|
|
864
|
+
:gating="pipelines.draftGating[unit.companionIndex]!"
|
|
865
|
+
:axes="ESTIMATE_AXES"
|
|
866
|
+
outcome="step"
|
|
867
|
+
@update="
|
|
868
|
+
(axis, value) => setCompanionGatingAxis(unit.companionIndex, axis, value)
|
|
869
|
+
"
|
|
870
|
+
/>
|
|
826
871
|
</div>
|
|
827
872
|
|
|
828
873
|
<!-- Consensus config (shown when the step is consensus-enabled). -->
|
|
@@ -953,31 +998,14 @@ async function clone(p: Pipeline) {
|
|
|
953
998
|
:title="t('pipeline.builder.consensusGateTooltip')"
|
|
954
999
|
@click="toggleGating(unit.index)"
|
|
955
1000
|
/>
|
|
956
|
-
<template v-if="pipelines.draftConsensus[unit.index]!.gating?.enabled">
|
|
957
|
-
<label class="text-slate-400">{{
|
|
958
|
-
t('pipeline.builder.riskThreshold')
|
|
959
|
-
}}</label>
|
|
960
|
-
<input
|
|
961
|
-
v-model.number="pipelines.draftConsensus[unit.index]!.gating!.minRisk"
|
|
962
|
-
type="number"
|
|
963
|
-
min="0"
|
|
964
|
-
max="1"
|
|
965
|
-
step="0.1"
|
|
966
|
-
class="w-14 rounded border border-slate-700 bg-slate-900 px-1.5 py-0.5 text-slate-100"
|
|
967
|
-
/>
|
|
968
|
-
<label class="text-slate-400">{{
|
|
969
|
-
t('pipeline.builder.impactThreshold')
|
|
970
|
-
}}</label>
|
|
971
|
-
<input
|
|
972
|
-
v-model.number="pipelines.draftConsensus[unit.index]!.gating!.minImpact"
|
|
973
|
-
type="number"
|
|
974
|
-
min="0"
|
|
975
|
-
max="1"
|
|
976
|
-
step="0.1"
|
|
977
|
-
class="w-14 rounded border border-slate-700 bg-slate-900 px-1.5 py-0.5 text-slate-100"
|
|
978
|
-
/>
|
|
979
|
-
</template>
|
|
980
1001
|
</div>
|
|
1002
|
+
<EstimateThresholdFields
|
|
1003
|
+
v-if="pipelines.draftConsensus[unit.index]!.gating?.enabled"
|
|
1004
|
+
:gating="pipelines.draftConsensus[unit.index]!.gating!"
|
|
1005
|
+
:axes="CONSENSUS_ESTIMATE_AXES"
|
|
1006
|
+
outcome="consensus"
|
|
1007
|
+
@update="(axis, value) => setConsensusGatingAxis(unit.index, axis, value)"
|
|
1008
|
+
/>
|
|
981
1009
|
</template>
|
|
982
1010
|
</div>
|
|
983
1011
|
|
|
@@ -1013,43 +1041,13 @@ async function clone(p: Pipeline) {
|
|
|
1013
1041
|
@click="pipelines.toggleDraftTesterQualityGating(unit.index)"
|
|
1014
1042
|
/>
|
|
1015
1043
|
</div>
|
|
1016
|
-
<
|
|
1044
|
+
<EstimateThresholdFields
|
|
1017
1045
|
v-if="pipelines.draftTesterQuality[unit.index]?.gating?.enabled"
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
<label class="text-slate-400">{{
|
|
1024
|
-
t('pipeline.builder.complexityThreshold')
|
|
1025
|
-
}}</label>
|
|
1026
|
-
<input
|
|
1027
|
-
v-model.number="pipelines.draftTesterQuality[unit.index]!.gating!.minComplexity"
|
|
1028
|
-
type="number"
|
|
1029
|
-
min="0"
|
|
1030
|
-
max="1"
|
|
1031
|
-
step="0.1"
|
|
1032
|
-
class="w-14 rounded border border-slate-700 bg-slate-900 px-1.5 py-0.5 text-slate-100"
|
|
1033
|
-
/>
|
|
1034
|
-
<label class="text-slate-400">{{ t('pipeline.builder.riskThreshold') }}</label>
|
|
1035
|
-
<input
|
|
1036
|
-
v-model.number="pipelines.draftTesterQuality[unit.index]!.gating!.minRisk"
|
|
1037
|
-
type="number"
|
|
1038
|
-
min="0"
|
|
1039
|
-
max="1"
|
|
1040
|
-
step="0.1"
|
|
1041
|
-
class="w-14 rounded border border-slate-700 bg-slate-900 px-1.5 py-0.5 text-slate-100"
|
|
1042
|
-
/>
|
|
1043
|
-
<label class="text-slate-400">{{ t('pipeline.builder.impactThreshold') }}</label>
|
|
1044
|
-
<input
|
|
1045
|
-
v-model.number="pipelines.draftTesterQuality[unit.index]!.gating!.minImpact"
|
|
1046
|
-
type="number"
|
|
1047
|
-
min="0"
|
|
1048
|
-
max="1"
|
|
1049
|
-
step="0.1"
|
|
1050
|
-
class="w-14 rounded border border-slate-700 bg-slate-900 px-1.5 py-0.5 text-slate-100"
|
|
1051
|
-
/>
|
|
1052
|
-
</div>
|
|
1046
|
+
:gating="pipelines.draftTesterQuality[unit.index]!.gating!"
|
|
1047
|
+
:axes="ESTIMATE_AXES"
|
|
1048
|
+
outcome="step"
|
|
1049
|
+
@update="(axis, value) => setTesterQualityGatingAxis(unit.index, axis, value)"
|
|
1050
|
+
/>
|
|
1053
1051
|
</div>
|
|
1054
1052
|
</li>
|
|
1055
1053
|
</ol>
|
|
@@ -427,7 +427,10 @@ function fail(title: string, e: unknown) {
|
|
|
427
427
|
{{ t('settings.consensusGroups.editor.gatedHint') }}
|
|
428
428
|
</p>
|
|
429
429
|
<div v-if="editor.gated" class="flex flex-wrap items-center gap-3 text-xs">
|
|
430
|
-
<label
|
|
430
|
+
<label
|
|
431
|
+
class="flex items-center gap-1.5 text-slate-400"
|
|
432
|
+
:title="t('pipeline.builder.riskThresholdHint')"
|
|
433
|
+
>
|
|
431
434
|
{{ t('pipeline.builder.riskThreshold') }}
|
|
432
435
|
<UInput
|
|
433
436
|
v-model.number="editor.minRisk"
|
|
@@ -439,7 +442,10 @@ function fail(title: string, e: unknown) {
|
|
|
439
442
|
class="w-20"
|
|
440
443
|
/>
|
|
441
444
|
</label>
|
|
442
|
-
<label
|
|
445
|
+
<label
|
|
446
|
+
class="flex items-center gap-1.5 text-slate-400"
|
|
447
|
+
:title="t('pipeline.builder.impactThresholdHint')"
|
|
448
|
+
>
|
|
443
449
|
{{ t('pipeline.builder.impactThreshold') }}
|
|
444
450
|
<UInput
|
|
445
451
|
v-model.number="editor.minImpact"
|
|
@@ -451,7 +457,10 @@ function fail(title: string, e: unknown) {
|
|
|
451
457
|
class="w-20"
|
|
452
458
|
/>
|
|
453
459
|
</label>
|
|
454
|
-
<label
|
|
460
|
+
<label
|
|
461
|
+
class="flex items-center gap-1.5 text-slate-400"
|
|
462
|
+
:title="t('pipeline.builder.complexityThresholdHint')"
|
|
463
|
+
>
|
|
455
464
|
{{ t('settings.consensusGroups.editor.complexityThreshold') }}
|
|
456
465
|
<UInput
|
|
457
466
|
v-model.number="editor.minComplexity"
|
|
@@ -29,10 +29,17 @@ export class ApiError extends Error {
|
|
|
29
29
|
|
|
30
30
|
/** The error envelope every controller emits (`handleError` / contract request-validator). */
|
|
31
31
|
export interface ApiErrorEnvelope {
|
|
32
|
+
/** The status class — an `ApiErrorCode` from `@cat-factory/contracts`, when recognised. */
|
|
32
33
|
code?: string
|
|
33
34
|
message?: string
|
|
34
35
|
details?: unknown
|
|
35
36
|
issues?: { path?: string; message: string }[]
|
|
37
|
+
/**
|
|
38
|
+
* The request's correlation id (`mountRequestLogging` mints or adopts `X-Request-Id` and
|
|
39
|
+
* `handleError` puts it on EVERY envelope). It is the join between what the user saw and the
|
|
40
|
+
* one server log line that explains it, so any surface showing failure detail should quote it.
|
|
41
|
+
*/
|
|
42
|
+
requestId?: string
|
|
36
43
|
}
|
|
37
44
|
|
|
38
45
|
/** Read the `{ error: {...} }` envelope out of a parsed response body, else undefined. */
|