@cat-factory/app 0.34.0 → 0.36.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/gates/GateResultView.vue +107 -12
- package/app/components/layout/IntegrationsHub.vue +13 -0
- package/app/components/layout/NotificationsInbox.vue +16 -0
- package/app/components/settings/LocalModeSettingsPanel.vue +159 -0
- package/app/components/slack/SlackPanel.vue +1 -0
- package/app/composables/api/humanReview.ts +18 -0
- package/app/composables/api/localSettings.ts +17 -0
- package/app/composables/useApi.ts +4 -0
- package/app/pages/index.vue +2 -0
- package/app/stores/humanReview.ts +41 -0
- package/app/stores/localSettings.ts +48 -0
- package/app/stores/ui.ts +13 -0
- package/app/types/domain.ts +4 -0
- package/app/types/execution.ts +16 -0
- package/app/types/localSettings.ts +31 -0
- package/app/types/notifications.ts +1 -0
- package/app/utils/catalog.ts +12 -0
- package/package.json +1 -1
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// persists on `step.gate`: the precheck verdict, the helper attempt budget, the gated
|
|
6
6
|
// commit, and — for CI — the failing checks behind the failure. One window serves both
|
|
7
7
|
// gates; it branches on the step's `agentKind` for the copy and the failure detail.
|
|
8
|
-
import { computed } from 'vue'
|
|
8
|
+
import { computed, ref } 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'
|
|
@@ -30,10 +30,37 @@ const step = computed(() => {
|
|
|
30
30
|
const gate = computed<GateStepState | null>(() => step.value?.gate ?? null)
|
|
31
31
|
|
|
32
32
|
const isCi = computed(() => step.value?.agentKind === 'ci')
|
|
33
|
+
const isHumanReview = computed(() => step.value?.agentKind === 'human-review')
|
|
33
34
|
const meta = computed(() => agentKindMeta(step.value?.agentKind ?? 'ci'))
|
|
34
|
-
const helperKind = computed(() =>
|
|
35
|
+
const helperKind = computed(() =>
|
|
36
|
+
isHumanReview.value ? 'fixer' : isCi.value ? 'ci-fixer' : 'conflict-resolver',
|
|
37
|
+
)
|
|
35
38
|
const helperMeta = computed(() => agentKindMeta(helperKind.value))
|
|
36
39
|
|
|
40
|
+
const subtitle = computed(() =>
|
|
41
|
+
isHumanReview.value
|
|
42
|
+
? 'Waits for a human code review on the PR, looping the fixer on comments'
|
|
43
|
+
: isCi.value
|
|
44
|
+
? 'Gates the PR on green CI, looping the CI fixer on failure'
|
|
45
|
+
: 'Gates the PR on a clean merge, looping the resolver on conflicts',
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
// Human-review: approval progress + the freeform "request a fix" control.
|
|
49
|
+
const humanReview = useHumanReviewStore()
|
|
50
|
+
const fixInstructions = ref('')
|
|
51
|
+
const fixBusy = computed(() => (blockId.value ? humanReview.isBusy(blockId.value) : false))
|
|
52
|
+
async function submitFix() {
|
|
53
|
+
const id = blockId.value
|
|
54
|
+
const text = fixInstructions.value.trim()
|
|
55
|
+
if (!id || !text) return
|
|
56
|
+
await humanReview.requestFix(id, text)
|
|
57
|
+
fixInstructions.value = ''
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// The displayed "required approvals" is derived from the cached branch-protection count via
|
|
61
|
+
// the gate's effective floor (`max(1, …)`, see review.logic.ts) rather than persisted twice.
|
|
62
|
+
const requiredApprovals = computed(() => Math.max(1, gate.value?.requiredApprovingReviewCount ?? 1))
|
|
63
|
+
|
|
37
64
|
const failingChecks = computed(() => gate.value?.failingChecks ?? [])
|
|
38
65
|
const shortSha = computed(() => (gate.value?.headSha ? gate.value.headSha.slice(0, 7) : null))
|
|
39
66
|
|
|
@@ -130,13 +157,7 @@ const conflictVerdict = computed(() => {
|
|
|
130
157
|
<h2 class="truncate text-sm font-semibold text-slate-100">
|
|
131
158
|
{{ meta.label }}{{ block ? ` — ${block.title}` : '' }}
|
|
132
159
|
</h2>
|
|
133
|
-
<p class="truncate text-[11px] text-slate-400">
|
|
134
|
-
{{
|
|
135
|
-
isCi
|
|
136
|
-
? 'Gates the PR on green CI, looping the CI fixer on failure'
|
|
137
|
-
: 'Gates the PR on a clean merge, looping the resolver on conflicts'
|
|
138
|
-
}}
|
|
139
|
-
</p>
|
|
160
|
+
<p class="truncate text-[11px] text-slate-400">{{ subtitle }}</p>
|
|
140
161
|
</div>
|
|
141
162
|
<UBadge :color="STATUS_META[status].badge" variant="subtle" size="sm">
|
|
142
163
|
{{ STATUS_META[status].label }}
|
|
@@ -186,6 +207,73 @@ const conflictVerdict = computed(() => {
|
|
|
186
207
|
</p>
|
|
187
208
|
</div>
|
|
188
209
|
|
|
210
|
+
<!-- Human review: approval progress, the feedback being fixed, freeform fix box -->
|
|
211
|
+
<template v-else-if="isHumanReview">
|
|
212
|
+
<div
|
|
213
|
+
class="flex items-center gap-2 rounded-md border border-slate-800 bg-slate-950/40 px-3 py-2"
|
|
214
|
+
>
|
|
215
|
+
<UIcon name="i-lucide-users" class="h-4 w-4 shrink-0 text-violet-300" />
|
|
216
|
+
<span class="text-[13px] text-slate-200">
|
|
217
|
+
{{ gate.lastApprovals ?? 0 }} / {{ requiredApprovals }} approval{{
|
|
218
|
+
requiredApprovals === 1 ? '' : 's'
|
|
219
|
+
}}
|
|
220
|
+
<template v-if="status === 'fixing'"> · fixer addressing comments…</template>
|
|
221
|
+
<template v-else-if="status === 'failing'">
|
|
222
|
+
· review comments to address</template
|
|
223
|
+
>
|
|
224
|
+
<template v-else> · awaiting review</template>
|
|
225
|
+
</span>
|
|
226
|
+
</div>
|
|
227
|
+
<p
|
|
228
|
+
v-if="gate.lastFailureSummary"
|
|
229
|
+
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"
|
|
230
|
+
>
|
|
231
|
+
{{ gate.lastFailureSummary }}
|
|
232
|
+
</p>
|
|
233
|
+
<a
|
|
234
|
+
v-if="prUrl"
|
|
235
|
+
:href="prUrl"
|
|
236
|
+
target="_blank"
|
|
237
|
+
rel="noopener"
|
|
238
|
+
class="mt-2 inline-flex items-center gap-1 text-[12px] text-sky-300 hover:text-sky-200 hover:underline"
|
|
239
|
+
>
|
|
240
|
+
Review pull request on GitHub
|
|
241
|
+
<UIcon name="i-lucide-external-link" class="h-3 w-3" />
|
|
242
|
+
</a>
|
|
243
|
+
|
|
244
|
+
<!-- Freeform fix request: dispatch the fixer now with these instructions. -->
|
|
245
|
+
<section v-if="status !== 'gave-up'" class="mt-4">
|
|
246
|
+
<h3
|
|
247
|
+
class="mb-1.5 text-[11px] font-semibold uppercase tracking-wide text-slate-500"
|
|
248
|
+
>
|
|
249
|
+
Request a fix
|
|
250
|
+
</h3>
|
|
251
|
+
<p class="mb-2 text-[11px] leading-relaxed text-slate-500">
|
|
252
|
+
Describe a change for the fixer to make on the PR branch now (in addition to any
|
|
253
|
+
review comments, which it addresses automatically).
|
|
254
|
+
</p>
|
|
255
|
+
<textarea
|
|
256
|
+
v-model="fixInstructions"
|
|
257
|
+
rows="3"
|
|
258
|
+
:disabled="fixBusy"
|
|
259
|
+
placeholder="e.g. rename the helper and add a unit test for the empty-input case"
|
|
260
|
+
class="w-full resize-y rounded-md border border-slate-800 bg-slate-950/60 px-3 py-2 text-[13px] text-slate-200 placeholder:text-slate-600 focus:border-violet-500/60 focus:outline-none"
|
|
261
|
+
/>
|
|
262
|
+
<div class="mt-2 flex justify-end">
|
|
263
|
+
<UButton
|
|
264
|
+
size="sm"
|
|
265
|
+
color="primary"
|
|
266
|
+
icon="i-lucide-wrench"
|
|
267
|
+
:loading="fixBusy"
|
|
268
|
+
:disabled="fixBusy || fixInstructions.trim().length === 0"
|
|
269
|
+
@click="submitFix"
|
|
270
|
+
>
|
|
271
|
+
Request fix
|
|
272
|
+
</UButton>
|
|
273
|
+
</div>
|
|
274
|
+
</section>
|
|
275
|
+
</template>
|
|
276
|
+
|
|
189
277
|
<!-- CI: failing checks -->
|
|
190
278
|
<template v-else-if="isCi">
|
|
191
279
|
<h3 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
|
@@ -321,9 +409,16 @@ const conflictVerdict = computed(() => {
|
|
|
321
409
|
{{ helperMeta.label }}
|
|
322
410
|
</h4>
|
|
323
411
|
<p class="text-[12px] text-slate-300">
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
412
|
+
<!-- The human-review gate's budget is effectively unbounded (it waits for a human
|
|
413
|
+
indefinitely), so render a plain round count rather than "0/9007199254740991". -->
|
|
414
|
+
<template v-if="isHumanReview">
|
|
415
|
+
{{ gate.attempts }} fix round{{ gate.attempts === 1 ? '' : 's' }}
|
|
416
|
+
</template>
|
|
417
|
+
<template v-else>
|
|
418
|
+
{{ gate.attempts }}/{{ gate.maxAttempts }} attempt{{
|
|
419
|
+
gate.maxAttempts === 1 ? '' : 's'
|
|
420
|
+
}}
|
|
421
|
+
</template>
|
|
327
422
|
<template v-if="gate.phase === 'working'"> · running…</template>
|
|
328
423
|
<template v-else-if="gate.attempts === 0"> · not needed yet</template>
|
|
329
424
|
</p>
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// Sections gate on the same `available` probes the navbar used, so a system that
|
|
9
9
|
// the backend has turned off simply doesn't appear here.
|
|
10
10
|
const ui = useUiStore()
|
|
11
|
+
const auth = useAuthStore()
|
|
11
12
|
const github = useGitHubStore()
|
|
12
13
|
const slack = useSlackStore()
|
|
13
14
|
const documents = useDocumentsStore()
|
|
@@ -256,6 +257,18 @@ const groups = computed<IntegrationGroup[]>(() => {
|
|
|
256
257
|
onClick: () => go(() => ui.openProviderConnection('runner-pool')),
|
|
257
258
|
})
|
|
258
259
|
}
|
|
260
|
+
// Local-mode-only: the warm-container pool + checkout reuse for the local runner. Shown
|
|
261
|
+
// only on the local-mode service (the controller 503s elsewhere, and `auth.localMode`
|
|
262
|
+
// is set from /auth/config).
|
|
263
|
+
if (auth.localMode?.enabled) {
|
|
264
|
+
infra.push({
|
|
265
|
+
key: 'local-mode',
|
|
266
|
+
icon: 'i-lucide-container',
|
|
267
|
+
label: 'Local mode',
|
|
268
|
+
description: 'Warm container pool + per-repo checkout reuse for the local runner.',
|
|
269
|
+
onClick: () => go(ui.openLocalModeSettings),
|
|
270
|
+
})
|
|
271
|
+
}
|
|
259
272
|
if (infra.length) out.push({ title: 'Infrastructure', items: infra })
|
|
260
273
|
|
|
261
274
|
return out
|
|
@@ -36,6 +36,9 @@ const META: Record<Notification['type'], { icon: string; color: Accent; action:
|
|
|
36
36
|
// Clicking the title opens the human-testing window for the task (see `reveal`); "act" just
|
|
37
37
|
// marks it read (the gate is resolved in that window — confirm / request a fix — not here).
|
|
38
38
|
human_test_ready: { icon: 'i-lucide-user-check', color: 'primary', action: 'Mark read' },
|
|
39
|
+
// Clicking the title opens the task's gate window (where the human can request a freeform
|
|
40
|
+
// fix); "act" just marks it read (approval happens on GitHub, not here).
|
|
41
|
+
human_review: { icon: 'i-lucide-users', color: 'primary', action: 'Mark read' },
|
|
39
42
|
// Clicking the title opens the Follow-up companion window for the run (see `reveal`); "act"
|
|
40
43
|
// just marks it read (items are decided in that window — file / send back / answer — not here).
|
|
41
44
|
followup_pending: { icon: 'i-lucide-compass', color: 'warning', action: 'Mark read' },
|
|
@@ -84,10 +87,23 @@ function reveal(n: Notification) {
|
|
|
84
87
|
else if (n.type === 'clarity_review') ui.openClarityReview(n.blockId)
|
|
85
88
|
else if (n.type === 'decision_required') revealDecision(n)
|
|
86
89
|
else if (n.type === 'human_test_ready') revealHumanTest(n)
|
|
90
|
+
else if (n.type === 'human_review') revealHumanReview(n)
|
|
87
91
|
else if (n.type === 'followup_pending') revealFollowUps(n)
|
|
88
92
|
else ui.select(n.blockId)
|
|
89
93
|
}
|
|
90
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Open the gate window for a parked `human-review` gate: find the run's human-review step and
|
|
97
|
+
* open it through the universal step dispatch (its archetype declares the `gate` result view,
|
|
98
|
+
* where the human can request a freeform fix). Falls back to focusing the block.
|
|
99
|
+
*/
|
|
100
|
+
function revealHumanReview(n: Notification) {
|
|
101
|
+
const instance = n.executionId ? execution.getInstance(n.executionId) : undefined
|
|
102
|
+
const idx = instance?.steps.findIndex((s) => s.agentKind === 'human-review') ?? -1
|
|
103
|
+
if (instance && idx >= 0) ui.openStepDetail(instance.id, idx)
|
|
104
|
+
else if (n.blockId) ui.select(n.blockId)
|
|
105
|
+
}
|
|
106
|
+
|
|
91
107
|
/**
|
|
92
108
|
* Open the Follow-up companion window for a run whose Coder parked on undecided items.
|
|
93
109
|
* Falls back to focusing the block when the run isn't loaded.
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Local-mode-only settings: the warm-container pool + per-repo checkout reuse. These are
|
|
3
|
+
// a per-DEPLOYMENT singleton stored in the DB (they replaced the LOCAL_POOL_* / HARNESS_*
|
|
4
|
+
// env vars), so a developer tunes them here instead of editing .env. The warm pool keeps
|
|
5
|
+
// idle harness containers ready and re-leases one (preferring repo affinity) to each run —
|
|
6
|
+
// far faster startup than a cold container per run. Saving applies the new sizing to the
|
|
7
|
+
// running service immediately (the pool is resized live — no restart needed); in-flight
|
|
8
|
+
// runs keep the container they already hold, and the checkout config applies to containers
|
|
9
|
+
// started after the save.
|
|
10
|
+
import { reactive, ref, watch } from 'vue'
|
|
11
|
+
|
|
12
|
+
const ui = useUiStore()
|
|
13
|
+
const store = useLocalSettingsStore()
|
|
14
|
+
const toast = useToast()
|
|
15
|
+
|
|
16
|
+
const open = computed({
|
|
17
|
+
get: () => ui.localModeSettingsOpen,
|
|
18
|
+
set: (v: boolean) => (v ? ui.openLocalModeSettings() : ui.closeLocalModeSettings()),
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const saving = ref(false)
|
|
22
|
+
|
|
23
|
+
// Editable draft. `idleMinutes` and `cleanKeep` are friendlier renderings of the stored
|
|
24
|
+
// `pool.idleTtlMs` (ms) and `checkout.cleanKeep` (string[]).
|
|
25
|
+
const draft = reactive({
|
|
26
|
+
size: 0,
|
|
27
|
+
minWarm: 0,
|
|
28
|
+
max: null as number | null,
|
|
29
|
+
idleMinutes: 10,
|
|
30
|
+
workspaceRoot: '/workspace',
|
|
31
|
+
cleanKeep: 'node_modules,.venv,target,.gradle,.pnpm-store',
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
function syncDraft() {
|
|
35
|
+
const s = store.settings
|
|
36
|
+
if (!s) return
|
|
37
|
+
draft.size = s.pool.size
|
|
38
|
+
draft.minWarm = s.pool.minWarm
|
|
39
|
+
draft.max = s.pool.max
|
|
40
|
+
draft.idleMinutes = Math.round(s.pool.idleTtlMs / 60_000)
|
|
41
|
+
draft.workspaceRoot = s.checkout.workspaceRoot
|
|
42
|
+
draft.cleanKeep = s.checkout.cleanKeep.join(',')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Load + hydrate the draft whenever the panel opens.
|
|
46
|
+
watch(open, (isOpen) => {
|
|
47
|
+
if (isOpen) void store.load().then(syncDraft)
|
|
48
|
+
})
|
|
49
|
+
watch(() => store.settings, syncDraft)
|
|
50
|
+
|
|
51
|
+
async function save() {
|
|
52
|
+
const cleanKeep = draft.cleanKeep
|
|
53
|
+
.split(',')
|
|
54
|
+
.map((s) => s.trim())
|
|
55
|
+
.filter(Boolean)
|
|
56
|
+
saving.value = true
|
|
57
|
+
try {
|
|
58
|
+
await store.save({
|
|
59
|
+
pool: {
|
|
60
|
+
size: Math.max(0, Math.floor(draft.size)),
|
|
61
|
+
minWarm: Math.max(0, Math.floor(draft.minWarm)),
|
|
62
|
+
max: draft.max == null ? null : Math.max(0, Math.floor(draft.max)),
|
|
63
|
+
idleTtlMs: Math.max(0, Math.floor(draft.idleMinutes * 60_000)),
|
|
64
|
+
},
|
|
65
|
+
checkout: { workspaceRoot: draft.workspaceRoot.trim() || '/workspace', cleanKeep },
|
|
66
|
+
})
|
|
67
|
+
toast.add({ title: 'Local settings saved', icon: 'i-lucide-check', color: 'success' })
|
|
68
|
+
} catch (e) {
|
|
69
|
+
toast.add({
|
|
70
|
+
title: 'Could not save local settings',
|
|
71
|
+
description: e instanceof Error ? e.message : String(e),
|
|
72
|
+
icon: 'i-lucide-triangle-alert',
|
|
73
|
+
color: 'error',
|
|
74
|
+
})
|
|
75
|
+
} finally {
|
|
76
|
+
saving.value = false
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
</script>
|
|
80
|
+
|
|
81
|
+
<template>
|
|
82
|
+
<UModal v-model:open="open" title="Local mode" :ui="{ content: 'max-w-2xl' }">
|
|
83
|
+
<template #body>
|
|
84
|
+
<div class="space-y-6">
|
|
85
|
+
<p class="text-xs text-slate-400">
|
|
86
|
+
Tuning for the local container runner — stored on this machine's deployment (it replaced
|
|
87
|
+
the <code>LOCAL_POOL_*</code> / <code>HARNESS_*</code> env vars). Saving resizes the warm
|
|
88
|
+
pool live — no restart needed; in-flight runs keep the container they already hold.
|
|
89
|
+
</p>
|
|
90
|
+
|
|
91
|
+
<!-- Warm container pool -->
|
|
92
|
+
<section class="space-y-3">
|
|
93
|
+
<div>
|
|
94
|
+
<h4 class="text-sm font-semibold text-slate-200">Warm container pool</h4>
|
|
95
|
+
<p class="text-[11px] text-slate-400">
|
|
96
|
+
Keep idle harness containers ready and re-lease one (preferring a container that
|
|
97
|
+
already holds the run's repo) instead of cold-starting per run. Pool size 0 disables
|
|
98
|
+
it. Requires a Docker-family runtime (Docker/Podman/OrbStack/Colima); ignored on Apple
|
|
99
|
+
<code>container</code>.
|
|
100
|
+
</p>
|
|
101
|
+
</div>
|
|
102
|
+
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
|
103
|
+
<UFormField label="Pool size" help="Max idle warm containers. 0 = pooling off.">
|
|
104
|
+
<UInput v-model.number="draft.size" type="number" :min="0" size="sm" />
|
|
105
|
+
</UFormField>
|
|
106
|
+
<UFormField label="Pre-warm at boot" help="Containers started when the service boots.">
|
|
107
|
+
<UInput v-model.number="draft.minWarm" type="number" :min="0" size="sm" />
|
|
108
|
+
</UFormField>
|
|
109
|
+
<UFormField label="Max containers" help="Hard cap (leased + idle). Blank = pool size.">
|
|
110
|
+
<UInput
|
|
111
|
+
v-model.number="draft.max"
|
|
112
|
+
type="number"
|
|
113
|
+
:min="0"
|
|
114
|
+
size="sm"
|
|
115
|
+
placeholder="(pool size)"
|
|
116
|
+
/>
|
|
117
|
+
</UFormField>
|
|
118
|
+
<UFormField label="Idle timeout (minutes)" help="Evict an idle pooled container after.">
|
|
119
|
+
<UInput v-model.number="draft.idleMinutes" type="number" :min="0" size="sm" />
|
|
120
|
+
</UFormField>
|
|
121
|
+
</div>
|
|
122
|
+
</section>
|
|
123
|
+
|
|
124
|
+
<!-- Checkout reuse -->
|
|
125
|
+
<section class="space-y-3 border-t border-slate-800 pt-6">
|
|
126
|
+
<div>
|
|
127
|
+
<h4 class="text-sm font-semibold text-slate-200">Checkout reuse</h4>
|
|
128
|
+
<p class="text-[11px] text-slate-400">
|
|
129
|
+
When a warm container already holds the run's repo, the harness reuses its per-repo
|
|
130
|
+
checkout (clean sweep + fetch + switch branch) instead of cloning fresh.
|
|
131
|
+
</p>
|
|
132
|
+
</div>
|
|
133
|
+
<UFormField
|
|
134
|
+
label="Workspace root"
|
|
135
|
+
help="Absolute in-container directory the reused checkout lives under."
|
|
136
|
+
>
|
|
137
|
+
<UInput v-model="draft.workspaceRoot" size="sm" placeholder="/workspace" />
|
|
138
|
+
</UFormField>
|
|
139
|
+
<UFormField
|
|
140
|
+
label="Keep on clean (comma-separated)"
|
|
141
|
+
help="Dependency-cache directories the per-run clean sweep preserves."
|
|
142
|
+
>
|
|
143
|
+
<UInput
|
|
144
|
+
v-model="draft.cleanKeep"
|
|
145
|
+
size="sm"
|
|
146
|
+
placeholder="node_modules,.venv,target,.gradle,.pnpm-store"
|
|
147
|
+
/>
|
|
148
|
+
</UFormField>
|
|
149
|
+
</section>
|
|
150
|
+
|
|
151
|
+
<div class="flex justify-end">
|
|
152
|
+
<UButton color="primary" icon="i-lucide-save" :loading="saving" @click="save">
|
|
153
|
+
Save
|
|
154
|
+
</UButton>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
</template>
|
|
158
|
+
</UModal>
|
|
159
|
+
</template>
|
|
@@ -45,6 +45,7 @@ const routes = reactive<Record<NotificationType, SlackRoute>>({
|
|
|
45
45
|
// In-app only (not in ROUTABLE), but the map is exhaustive over the type.
|
|
46
46
|
decision_required: { enabled: false, channel: '' },
|
|
47
47
|
human_test_ready: { enabled: false, channel: '' },
|
|
48
|
+
human_review: { enabled: false, channel: '' },
|
|
48
49
|
followup_pending: { enabled: false, channel: '' },
|
|
49
50
|
})
|
|
50
51
|
const mentionsEnabled = ref(false)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ExecutionInstance } from '~/types/domain'
|
|
2
|
+
import type { ApiContext } from './context'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The human-review gate's run-driving action. The gate self-drives off the PR's GitHub review
|
|
6
|
+
* state, but a human can request a freeform fix at any time — dispatched to the `fixer`
|
|
7
|
+
* immediately. Returns the updated execution instance (the gate state rides on its step + the
|
|
8
|
+
* execution stream).
|
|
9
|
+
*/
|
|
10
|
+
export function humanReviewApi({ http, ws }: ApiContext) {
|
|
11
|
+
return {
|
|
12
|
+
requestHumanReviewFix: (workspaceId: string, blockId: string, instructions: string) =>
|
|
13
|
+
http<ExecutionInstance>(
|
|
14
|
+
`${ws(workspaceId)}/blocks/${encodeURIComponent(blockId)}/human-review/request-fix`,
|
|
15
|
+
{ method: 'POST', body: { instructions } },
|
|
16
|
+
),
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { LocalSettings, UpdateLocalSettingsInput } from '~/types/localSettings'
|
|
2
|
+
import type { ApiContext } from './context'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Local-mode operational settings (warm-container-pool sizing + per-repo checkout reuse) —
|
|
6
|
+
* a per-deployment singleton. Wired only on the local-mode service; both calls 503 on the
|
|
7
|
+
* Worker / stock Node facades (the store hides the panel then). No secrets, so the read view
|
|
8
|
+
* is the plain config and the write replaces it wholesale.
|
|
9
|
+
*/
|
|
10
|
+
export function localSettingsApi({ http }: ApiContext) {
|
|
11
|
+
return {
|
|
12
|
+
getLocalSettings: () => http<LocalSettings>('/local-settings'),
|
|
13
|
+
|
|
14
|
+
updateLocalSettings: (body: UpdateLocalSettingsInput) =>
|
|
15
|
+
http<LocalSettings>('/local-settings', { method: 'PUT', body }),
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -9,8 +9,10 @@ import { executionApi } from './api/execution'
|
|
|
9
9
|
import { followUpsApi } from './api/followUps'
|
|
10
10
|
import { fragmentsApi } from './api/fragments'
|
|
11
11
|
import { githubApi } from './api/github'
|
|
12
|
+
import { humanReviewApi } from './api/humanReview'
|
|
12
13
|
import { humanTestApi } from './api/humanTest'
|
|
13
14
|
import { kaizenApi } from './api/kaizen'
|
|
15
|
+
import { localSettingsApi } from './api/localSettings'
|
|
14
16
|
import { modelsApi } from './api/models'
|
|
15
17
|
import { notificationsApi } from './api/notifications'
|
|
16
18
|
import { presetsApi } from './api/presets'
|
|
@@ -88,7 +90,9 @@ export function useApi() {
|
|
|
88
90
|
...reviewsApi(ctx),
|
|
89
91
|
...followUpsApi(ctx),
|
|
90
92
|
...humanTestApi(ctx),
|
|
93
|
+
...humanReviewApi(ctx),
|
|
91
94
|
...kaizenApi(ctx),
|
|
95
|
+
...localSettingsApi(ctx),
|
|
92
96
|
...specApi(ctx),
|
|
93
97
|
...notificationsApi(ctx),
|
|
94
98
|
...presetsApi(ctx),
|
package/app/pages/index.vue
CHANGED
|
@@ -35,6 +35,7 @@ import ProviderConnectionPanel from '~/components/settings/ProviderConnectionPan
|
|
|
35
35
|
import ProviderConfigBanner from '~/components/layout/ProviderConfigBanner.vue'
|
|
36
36
|
import ModelConfigurationPanel from '~/components/settings/ModelConfigurationPanel.vue'
|
|
37
37
|
import LocalModelEndpointsPanel from '~/components/settings/LocalModelEndpointsPanel.vue'
|
|
38
|
+
import LocalModeSettingsPanel from '~/components/settings/LocalModeSettingsPanel.vue'
|
|
38
39
|
import SandboxPanel from '~/components/sandbox/SandboxPanel.vue'
|
|
39
40
|
import UserSecretsSection from '~/components/settings/UserSecretsSection.vue'
|
|
40
41
|
import OpenRouterCatalogPanel from '~/components/settings/OpenRouterCatalogPanel.vue'
|
|
@@ -194,6 +195,7 @@ watch(
|
|
|
194
195
|
<ProviderConnectionPanel />
|
|
195
196
|
<ModelConfigurationPanel />
|
|
196
197
|
<LocalModelEndpointsPanel />
|
|
198
|
+
<LocalModeSettingsPanel />
|
|
197
199
|
<SandboxPanel />
|
|
198
200
|
<UserSecretsSection />
|
|
199
201
|
<OpenRouterCatalogPanel />
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { defineStore } from 'pinia'
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import { useExecutionStore } from '~/stores/execution'
|
|
4
|
+
import { useWorkspaceStore } from '~/stores/workspace'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Human-review gate actions. The gate's live state rides on its execution step (`step.gate`) and
|
|
8
|
+
* arrives via the execution stream, so this store holds NO gate state — it only drives the
|
|
9
|
+
* freeform "request a fix" action and patches the execution store from the response. A per-block
|
|
10
|
+
* `busy` flag lets the window disable its control while the request is in flight.
|
|
11
|
+
*/
|
|
12
|
+
export const useHumanReviewStore = defineStore('humanReview', () => {
|
|
13
|
+
const api = useApi()
|
|
14
|
+
const ws = useWorkspaceStore()
|
|
15
|
+
const execution = useExecutionStore()
|
|
16
|
+
|
|
17
|
+
const busy = ref<Set<string>>(new Set())
|
|
18
|
+
|
|
19
|
+
function isBusy(blockId: string): boolean {
|
|
20
|
+
return busy.value.has(blockId)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Dispatch the fixer against the PR from a human's freeform instructions (bypasses grace). */
|
|
24
|
+
async function requestFix(blockId: string, instructions: string): Promise<void> {
|
|
25
|
+
const next = new Set(busy.value)
|
|
26
|
+
next.add(blockId)
|
|
27
|
+
busy.value = next
|
|
28
|
+
try {
|
|
29
|
+
const instance = await api.requestHumanReviewFix(ws.requireId(), blockId, instructions)
|
|
30
|
+
if (instance && typeof instance === 'object' && 'steps' in instance) {
|
|
31
|
+
execution.upsert(instance as Parameters<typeof execution.upsert>[0])
|
|
32
|
+
}
|
|
33
|
+
} finally {
|
|
34
|
+
const after = new Set(busy.value)
|
|
35
|
+
after.delete(blockId)
|
|
36
|
+
busy.value = after
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return { isBusy, requestFix }
|
|
41
|
+
})
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { defineStore } from 'pinia'
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import type { LocalSettings, UpdateLocalSettingsInput } from '~/types/localSettings'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Local-mode operational settings (warm-container-pool sizing + per-repo checkout reuse) —
|
|
7
|
+
* a per-deployment singleton that replaced the old LOCAL_POOL_* / HARNESS_* env vars. No
|
|
8
|
+
* secrets, so the store holds the full config. `available` mirrors the backend opt-in: a 503
|
|
9
|
+
* (not the local-mode service) hides the panel. Loaded on demand from the settings panel.
|
|
10
|
+
*/
|
|
11
|
+
export const useLocalSettingsStore = defineStore('localSettings', () => {
|
|
12
|
+
const api = useApi()
|
|
13
|
+
|
|
14
|
+
const settings = ref<LocalSettings | null>(null)
|
|
15
|
+
const loading = ref(false)
|
|
16
|
+
const available = ref<boolean | null>(null)
|
|
17
|
+
|
|
18
|
+
async function load() {
|
|
19
|
+
loading.value = true
|
|
20
|
+
try {
|
|
21
|
+
settings.value = await api.getLocalSettings()
|
|
22
|
+
available.value = true
|
|
23
|
+
} catch (e) {
|
|
24
|
+
// 503 ⇒ not the local-mode service ⇒ hide the panel.
|
|
25
|
+
if (
|
|
26
|
+
e &&
|
|
27
|
+
typeof e === 'object' &&
|
|
28
|
+
'statusCode' in e &&
|
|
29
|
+
(e as { statusCode?: number }).statusCode === 503
|
|
30
|
+
) {
|
|
31
|
+
available.value = false
|
|
32
|
+
settings.value = null
|
|
33
|
+
} else {
|
|
34
|
+
throw e
|
|
35
|
+
}
|
|
36
|
+
} finally {
|
|
37
|
+
loading.value = false
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function save(input: UpdateLocalSettingsInput) {
|
|
42
|
+
settings.value = await api.updateLocalSettings(input)
|
|
43
|
+
available.value = true
|
|
44
|
+
return settings.value
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return { settings, loading, available, load, save }
|
|
48
|
+
})
|
package/app/stores/ui.ts
CHANGED
|
@@ -116,6 +116,9 @@ export const useUiStore = defineStore('ui', () => {
|
|
|
116
116
|
const vendorCredentialsOpen = ref(false)
|
|
117
117
|
// Per-user settings panel: the signed-in user's own-machine local model runners.
|
|
118
118
|
const localModelsOpen = ref(false)
|
|
119
|
+
// Local-mode-only settings panel: the warm-container pool sizing + per-repo checkout reuse
|
|
120
|
+
// (a per-deployment singleton that replaced the LOCAL_POOL_* / HARNESS_* env vars).
|
|
121
|
+
const localModeSettingsOpen = ref(false)
|
|
119
122
|
// The Sandbox (parallel prompt/model testing) surface — an opt-in, on-demand window.
|
|
120
123
|
const sandboxOpen = ref(false)
|
|
121
124
|
const userSecretsOpen = ref(false)
|
|
@@ -432,6 +435,13 @@ export const useUiStore = defineStore('ui', () => {
|
|
|
432
435
|
function closeLocalModels() {
|
|
433
436
|
localModelsOpen.value = false
|
|
434
437
|
}
|
|
438
|
+
function openLocalModeSettings() {
|
|
439
|
+
cameFromIntegrations.value = false
|
|
440
|
+
localModeSettingsOpen.value = true
|
|
441
|
+
}
|
|
442
|
+
function closeLocalModeSettings() {
|
|
443
|
+
localModeSettingsOpen.value = false
|
|
444
|
+
}
|
|
435
445
|
function openSandbox() {
|
|
436
446
|
sandboxOpen.value = true
|
|
437
447
|
}
|
|
@@ -580,6 +590,7 @@ export const useUiStore = defineStore('ui', () => {
|
|
|
580
590
|
modelConfigOpen,
|
|
581
591
|
vendorCredentialsOpen,
|
|
582
592
|
localModelsOpen,
|
|
593
|
+
localModeSettingsOpen,
|
|
583
594
|
sandboxOpen,
|
|
584
595
|
userSecretsOpen,
|
|
585
596
|
openRouterOpen,
|
|
@@ -650,6 +661,8 @@ export const useUiStore = defineStore('ui', () => {
|
|
|
650
661
|
closeVendorCredentials,
|
|
651
662
|
openLocalModels,
|
|
652
663
|
closeLocalModels,
|
|
664
|
+
openLocalModeSettings,
|
|
665
|
+
closeLocalModeSettings,
|
|
653
666
|
openSandbox,
|
|
654
667
|
closeSandbox,
|
|
655
668
|
openUserSecrets,
|
package/app/types/domain.ts
CHANGED
|
@@ -315,6 +315,10 @@ export type AgentKind =
|
|
|
315
315
|
// validate the change in a live URL, dispatching the Tester's `fixer` (from findings) or
|
|
316
316
|
// the `conflict-resolver` (on a conflicting pull-main) on demand. Opens its own window.
|
|
317
317
|
| 'human-test'
|
|
318
|
+
// The human-review gate: watches the PR for a human code review on GitHub, looping the
|
|
319
|
+
// `fixer` to address review comments and advancing once approved with no unresolved threads.
|
|
320
|
+
// Opens the shared gate window (with a freeform "request a fix" box).
|
|
321
|
+
| 'human-review'
|
|
318
322
|
// The Kaizen agent: post-run grader (NOT a pipeline step / palette archetype). Surfaced
|
|
319
323
|
// only in Model Configuration (its model is pinnable like any agent) and run details.
|
|
320
324
|
| 'kaizen'
|
package/app/types/execution.ts
CHANGED
|
@@ -464,6 +464,22 @@ export interface GateStepState {
|
|
|
464
464
|
failingChecks?: GateFailingCheck[] | null
|
|
465
465
|
/** history of the helper-agent attempts this gate dispatched, newest last */
|
|
466
466
|
attemptLog?: GateAttempt[] | null
|
|
467
|
+
// ---- human-review gate only ----
|
|
468
|
+
/** approvals the PR had at the last probe (human-review gate) */
|
|
469
|
+
lastApprovals?: number | null
|
|
470
|
+
/**
|
|
471
|
+
* raw branch-protection required count, cached after the first probe (human-review gate).
|
|
472
|
+
* The displayed "required" count is `max(1, this)` — the gate's effective floor.
|
|
473
|
+
*/
|
|
474
|
+
requiredApprovingReviewCount?: number | null
|
|
475
|
+
/** review threads handed to the fixer, pending resolve on its completion (human-review gate) */
|
|
476
|
+
pendingThreadIds?: string[] | null
|
|
477
|
+
/** the grace window (minutes) before the fixer addresses a comment batch (human-review gate) */
|
|
478
|
+
humanReviewGraceMinutes?: number | null
|
|
479
|
+
/** newest plain PR comment already handed to the fixer (human-review gate) */
|
|
480
|
+
lastAddressedCommentAt?: number | null
|
|
481
|
+
/** a human-initiated freeform fix parked on the gate, consumed on the next poll (human-review gate) */
|
|
482
|
+
pendingFix?: { instructions: string; at: number } | null
|
|
467
483
|
}
|
|
468
484
|
|
|
469
485
|
/** Live state of a `tester` step's Tester→Fixer loop (mirrors `testerStepStateSchema`). */
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Local-mode operational settings (warm-container-pool sizing + per-repo checkout reuse).
|
|
2
|
+
// Mirrors `@cat-factory/contracts` localSettings. A per-deployment singleton, edited in the
|
|
3
|
+
// dedicated local-mode settings panel — these replaced the old LOCAL_POOL_* / HARNESS_* env
|
|
4
|
+
// vars. There are no secrets, so the read view is the plain config. Local-mode-only (the
|
|
5
|
+
// warm pool is the local Docker-family runner's differentiator).
|
|
6
|
+
|
|
7
|
+
export interface LocalPoolSettings {
|
|
8
|
+
/** Max idle warm containers kept for re-lease. 0 disables pooling (cold-start per run). */
|
|
9
|
+
size: number
|
|
10
|
+
/** Containers pre-warmed when the service starts. */
|
|
11
|
+
minWarm: number
|
|
12
|
+
/** Hard cap on total containers (leased + idle). `null` ⇒ defaults to `size`. */
|
|
13
|
+
max: number | null
|
|
14
|
+
/** How long an idle pooled container is kept before eviction (ms). */
|
|
15
|
+
idleTtlMs: number
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface LocalCheckoutSettings {
|
|
19
|
+
/** Absolute in-container dir the reused per-repo checkout lives under. */
|
|
20
|
+
workspaceRoot: string
|
|
21
|
+
/** Dep-cache directories the per-run clean sweep keeps (so deps aren't reinstalled). */
|
|
22
|
+
cleanKeep: string[]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface LocalSettings {
|
|
26
|
+
pool: LocalPoolSettings
|
|
27
|
+
checkout: LocalCheckoutSettings
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Admin write: the full settings blob fully replaces the stored config. */
|
|
31
|
+
export type UpdateLocalSettingsInput = LocalSettings
|
package/app/utils/catalog.ts
CHANGED
|
@@ -352,6 +352,18 @@ export const SYSTEM_AGENT_META: Record<string, AgentArchetype> = {
|
|
|
352
352
|
color: '#a3e635',
|
|
353
353
|
description: 'Scores the PR and auto-merges within the task thresholds, or asks for review.',
|
|
354
354
|
},
|
|
355
|
+
'human-review': {
|
|
356
|
+
kind: 'human-review',
|
|
357
|
+
label: 'Human Review Gate',
|
|
358
|
+
icon: 'i-lucide-users',
|
|
359
|
+
color: '#c084fc',
|
|
360
|
+
category: 'gates',
|
|
361
|
+
description:
|
|
362
|
+
'Waits for a human code review on the PR, looping the fixer to address comments; advances once approved with no unresolved threads.',
|
|
363
|
+
// Opens the dedicated gate window (approval progress, the feedback being fixed, and a
|
|
364
|
+
// freeform "request a fix" box) like the other gates.
|
|
365
|
+
resultView: 'gate',
|
|
366
|
+
},
|
|
355
367
|
// The Kaizen agent grades agent steps AFTER a run completes (continuous improvement).
|
|
356
368
|
// It is NOT a pipeline step (never in the palette — no `category`), but it runs an LLM,
|
|
357
369
|
// so it needs display metadata here and a per-workspace model in Model Configuration.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cat-factory/app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.36.0",
|
|
4
4
|
"description": "Reusable Nuxt layer for the Agent Architecture Board SPA (components, stores, composables, pages). Consume it from a thin deployment app via `extends: ['@cat-factory/app']` and point it at your backend with NUXT_PUBLIC_API_BASE. See deploy/frontend for an example.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|