@cat-factory/app 0.228.0 → 0.228.1
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/AddTaskModal.vue +11 -7
- package/app/components/board/RecurringPipelineModal.vue +11 -7
- package/app/components/bootstrap/BootstrapModal.vue +11 -7
- package/app/components/documents/RepoContextDocPicker.vue +4 -1
- package/app/components/fragments/FragmentLibraryManager.vue +19 -18
- package/app/components/gates/GateResultView.vue +3 -3
- package/app/components/github/AddServiceFromRepoModal.vue +10 -9
- package/app/components/panels/AgentStepDetail.vue +9 -7
- package/app/components/panels/InspectorPanel.vue +4 -4
- package/app/components/panels/inspector/ServiceTestConfig.vue +12 -11
- package/app/components/panels/inspector/TaskExecution.vue +7 -7
- package/app/components/pipeline/PipelineProgress.vue +5 -4
- package/app/components/providers/ApiKeysSection.vue +3 -1
- package/app/components/ralph/RalphLoopResultView.vue +3 -3
- package/app/components/visualConfirm/VisualConfirmationWindow.vue +14 -14
- package/app/pages/index.vue +21 -18
- package/app/stores/execution.ts +6 -6
- package/package.json +1 -1
|
@@ -59,12 +59,7 @@ const { t } = useI18n()
|
|
|
59
59
|
|
|
60
60
|
const { linkPending, presentLinkFailures } = useContextLinking()
|
|
61
61
|
|
|
62
|
-
const open = computed(
|
|
63
|
-
get: () => ui.addTaskContainerId !== null,
|
|
64
|
-
set: (v: boolean) => {
|
|
65
|
-
if (!v) void requestClose()
|
|
66
|
-
},
|
|
67
|
-
})
|
|
62
|
+
const open = computed(() => ui.addTaskContainerId !== null)
|
|
68
63
|
|
|
69
64
|
const container = computed(() =>
|
|
70
65
|
ui.addTaskContainerId ? board.getBlock(ui.addTaskContainerId) : undefined,
|
|
@@ -591,6 +586,15 @@ const { requestClose } = useUnsavedGuard({
|
|
|
591
586
|
}),
|
|
592
587
|
})
|
|
593
588
|
|
|
589
|
+
// The template's v-model binding: dismissal (Escape / backdrop) routes through the guard.
|
|
590
|
+
// Declared after the guard so the setter's `requestClose` reference is never in its TDZ.
|
|
591
|
+
const modalOpen = computed({
|
|
592
|
+
get: () => open.value,
|
|
593
|
+
set: (v: boolean) => {
|
|
594
|
+
if (!v) void requestClose()
|
|
595
|
+
},
|
|
596
|
+
})
|
|
597
|
+
|
|
594
598
|
// A recurring task only needs a target frame (its details are filled in the schedule
|
|
595
599
|
// modal); every other type needs a title. A review task additionally needs a target PR.
|
|
596
600
|
// The Ralph loop's completion criterion (its `ralph.validationCommand` agent-config id). The
|
|
@@ -757,7 +761,7 @@ function openReviewFrictionDialog(conflict: NonNullable<ReturnType<typeof parseC
|
|
|
757
761
|
</script>
|
|
758
762
|
|
|
759
763
|
<template>
|
|
760
|
-
<UModal v-model:open="
|
|
764
|
+
<UModal v-model:open="modalOpen" :title="t('board.addTask.title')">
|
|
761
765
|
<template #body>
|
|
762
766
|
<div class="space-y-4" data-testid="add-task-modal">
|
|
763
767
|
<p v-if="container" class="text-xs text-slate-400">
|
|
@@ -22,12 +22,7 @@ const toast = useToast()
|
|
|
22
22
|
const access = useWorkspaceAccess()
|
|
23
23
|
const { t, te } = useI18n()
|
|
24
24
|
|
|
25
|
-
const open = computed(
|
|
26
|
-
get: () => ui.addRecurringFrameId !== null,
|
|
27
|
-
set: (v: boolean) => {
|
|
28
|
-
if (!v) void requestClose()
|
|
29
|
-
},
|
|
30
|
-
})
|
|
25
|
+
const open = computed(() => ui.addRecurringFrameId !== null)
|
|
31
26
|
|
|
32
27
|
const frame = computed(() =>
|
|
33
28
|
ui.addRecurringFrameId ? board.getBlock(ui.addRecurringFrameId) : undefined,
|
|
@@ -240,6 +235,15 @@ const { requestClose } = useUnsavedGuard({
|
|
|
240
235
|
}),
|
|
241
236
|
})
|
|
242
237
|
|
|
238
|
+
// The template's v-model binding: dismissal (Escape / backdrop) routes through the guard.
|
|
239
|
+
// Declared after the guard so the setter's `requestClose` reference is never in its TDZ.
|
|
240
|
+
const modalOpen = computed({
|
|
241
|
+
get: () => open.value,
|
|
242
|
+
set: (v: boolean) => {
|
|
243
|
+
if (!v) void requestClose()
|
|
244
|
+
},
|
|
245
|
+
})
|
|
246
|
+
|
|
243
247
|
// The board field required for the picked source must be filled before a bug-intake schedule saves.
|
|
244
248
|
const intakeReady = computed(() => {
|
|
245
249
|
if (!showIntake.value) return true
|
|
@@ -339,7 +343,7 @@ async function add() {
|
|
|
339
343
|
</script>
|
|
340
344
|
|
|
341
345
|
<template>
|
|
342
|
-
<UModal v-model:open="
|
|
346
|
+
<UModal v-model:open="modalOpen" :title="t('board.recurring.title')">
|
|
343
347
|
<template #body>
|
|
344
348
|
<div class="space-y-4">
|
|
345
349
|
<p v-if="frame" class="text-xs text-slate-400">
|
|
@@ -18,12 +18,7 @@ const { freeFramePosition, focusFrame } = useFramePlacement()
|
|
|
18
18
|
const { t } = useI18n()
|
|
19
19
|
const { confirmAction, toastDone } = useConfirmAction()
|
|
20
20
|
|
|
21
|
-
const open = computed(
|
|
22
|
-
get: () => ui.bootstrapOpen,
|
|
23
|
-
set: (v: boolean) => {
|
|
24
|
-
if (!v) void requestClose()
|
|
25
|
-
},
|
|
26
|
-
})
|
|
21
|
+
const open = computed(() => ui.bootstrapOpen)
|
|
27
22
|
|
|
28
23
|
// Load the workspace's reference architectures + recent jobs, plus (best-effort)
|
|
29
24
|
// the GitHub repos the user can access so the base form can pick from them.
|
|
@@ -100,6 +95,15 @@ const { requestClose } = useUnsavedGuard({
|
|
|
100
95
|
}),
|
|
101
96
|
})
|
|
102
97
|
|
|
98
|
+
// The template's v-model binding: dismissal (Escape / backdrop) routes through the guard.
|
|
99
|
+
// Declared after the guard so the setter's `requestClose` reference is never in its TDZ.
|
|
100
|
+
const modalOpen = computed({
|
|
101
|
+
get: () => open.value,
|
|
102
|
+
set: (v: boolean) => {
|
|
103
|
+
if (!v) void requestClose()
|
|
104
|
+
},
|
|
105
|
+
})
|
|
106
|
+
|
|
103
107
|
// Mirror of the backend `slugField` rule (@cat-factory/contracts bootstrap
|
|
104
108
|
// schema): the new repo name is a SINGLE GitHub name segment — no "owner/"
|
|
105
109
|
// prefix — so reject a bad value inline before we hit the API. Kept in sync with
|
|
@@ -420,7 +424,7 @@ const statusLabel = computed<Record<BootstrapStatus, string>>(() => ({
|
|
|
420
424
|
</script>
|
|
421
425
|
|
|
422
426
|
<template>
|
|
423
|
-
<UModal v-model:open="
|
|
427
|
+
<UModal v-model:open="modalOpen" :title="t('bootstrap.title')" :ui="{ content: 'max-w-2xl' }">
|
|
424
428
|
<template #body>
|
|
425
429
|
<div class="space-y-6">
|
|
426
430
|
<!-- Three states, because each promises the user something different about the repo.
|
|
@@ -75,6 +75,10 @@ watch(selectedRepoId, (id) => {
|
|
|
75
75
|
if (found) selectedRepo.value = found
|
|
76
76
|
})
|
|
77
77
|
|
|
78
|
+
// The file-search query (its matches live in the file-selection section below). Declared
|
|
79
|
+
// above `clearRepo` and the repo-switch watcher, which both reset it.
|
|
80
|
+
const fileQuery = ref('')
|
|
81
|
+
|
|
78
82
|
function clearRepo() {
|
|
79
83
|
selectedRepoId.value = undefined
|
|
80
84
|
selectedRepo.value = undefined
|
|
@@ -127,7 +131,6 @@ watch(selectedRepoId, (id) => {
|
|
|
127
131
|
if (id !== undefined) void ensureFilesLoaded()
|
|
128
132
|
})
|
|
129
133
|
|
|
130
|
-
const fileQuery = ref('')
|
|
131
134
|
// Matches are computed client-side from the cached tree (no per-keystroke server call).
|
|
132
135
|
// A query is required so a large repo never renders thousands of rows at once; results
|
|
133
136
|
// are capped for the same reason.
|
|
@@ -171,6 +171,19 @@ const editDraft = ref<{
|
|
|
171
171
|
brief: string
|
|
172
172
|
tags: string
|
|
173
173
|
} | null>(null)
|
|
174
|
+
// The linked SHORT VERSION is an OVERRIDE of what the platform does by default (condense a
|
|
175
|
+
// long standard automatically, fold a short one in full), so it follows the override rule:
|
|
176
|
+
// hidden in basic mode while unset, revealed as soon as the fragment carries one — a
|
|
177
|
+
// basic-mode curator is never left unable to see or clear a brief a teammate linked.
|
|
178
|
+
//
|
|
179
|
+
// Both flags are LATCHED at the moment the form opens rather than tracking the live draft.
|
|
180
|
+
// Recomputing per keystroke makes the control delete itself the instant a basic-mode curator
|
|
181
|
+
// empties it — mid-edit, under the cursor, on the one interaction (clearing, to hand the
|
|
182
|
+
// standard back to auto-generation) the rule exists to keep reachable.
|
|
183
|
+
const uiMode = useUiModeStore()
|
|
184
|
+
const showEditBrief = ref(false)
|
|
185
|
+
const showDraftBrief = computed(() => showOverrideField(uiMode.isAdvanced, null))
|
|
186
|
+
|
|
174
187
|
function startEdit(f: (typeof library.fragments)[number]) {
|
|
175
188
|
editDraft.value = {
|
|
176
189
|
id: f.id,
|
|
@@ -192,19 +205,6 @@ const editValid = computed(
|
|
|
192
205
|
!!editDraft.value.summary.trim() &&
|
|
193
206
|
!!editDraft.value.body.trim(),
|
|
194
207
|
)
|
|
195
|
-
// The linked SHORT VERSION is an OVERRIDE of what the platform does by default (condense a
|
|
196
|
-
// long standard automatically, fold a short one in full), so it follows the override rule:
|
|
197
|
-
// hidden in basic mode while unset, revealed as soon as the fragment carries one — a
|
|
198
|
-
// basic-mode curator is never left unable to see or clear a brief a teammate linked.
|
|
199
|
-
//
|
|
200
|
-
// Both flags are LATCHED at the moment the form opens rather than tracking the live draft.
|
|
201
|
-
// Recomputing per keystroke makes the control delete itself the instant a basic-mode curator
|
|
202
|
-
// empties it — mid-edit, under the cursor, on the one interaction (clearing, to hand the
|
|
203
|
-
// standard back to auto-generation) the rule exists to keep reachable.
|
|
204
|
-
const uiMode = useUiModeStore()
|
|
205
|
-
const showEditBrief = ref(false)
|
|
206
|
-
const showDraftBrief = computed(() => showOverrideField(uiMode.isAdvanced, null))
|
|
207
|
-
|
|
208
208
|
async function saveEdit() {
|
|
209
209
|
const d = editDraft.value
|
|
210
210
|
if (!d || !editValid.value) return
|
|
@@ -277,11 +277,6 @@ async function removeFragment(id: string) {
|
|
|
277
277
|
// Link a Confluence/Notion page or GitHub file as a fragment that is re-resolved
|
|
278
278
|
// from the source at run time (a living source of truth, not a frozen snapshot).
|
|
279
279
|
const docDraft = ref({ source: '' as DocumentSourceKind | '', ref: '', tags: '' })
|
|
280
|
-
const docDraftValid = computed(() => {
|
|
281
|
-
if (docLinkDisabled.value || !docDraft.value.source) return false
|
|
282
|
-
// The GitHub picker validates on staged files; every other path on the free-text ref.
|
|
283
|
-
return usingDocPicker.value ? docFilePaths.value.length > 0 : !!docDraft.value.ref.trim()
|
|
284
|
-
})
|
|
285
280
|
|
|
286
281
|
// ---- GitHub file picker (documents tab) -----------------------------------
|
|
287
282
|
// For a GitHub source, let the user search a repo + browse to one or MORE files
|
|
@@ -364,6 +359,12 @@ const stagedDocRefs = computed(() =>
|
|
|
364
359
|
/** When the rich picker drives the ref(s); otherwise the free-text field does. */
|
|
365
360
|
const usingDocPicker = computed(() => showGithubDocPicker.value)
|
|
366
361
|
|
|
362
|
+
const docDraftValid = computed(() => {
|
|
363
|
+
if (docLinkDisabled.value || !docDraft.value.source) return false
|
|
364
|
+
// The GitHub picker validates on staged files; every other path on the free-text ref.
|
|
365
|
+
return usingDocPicker.value ? docFilePaths.value.length > 0 : !!docDraft.value.ref.trim()
|
|
366
|
+
})
|
|
367
|
+
|
|
367
368
|
/**
|
|
368
369
|
* A pasted GitHub file/directory URL resolved to a repo + location: select the repo
|
|
369
370
|
* (through the same refs the search select drives), then stage the file or jump the
|
|
@@ -25,9 +25,6 @@ const access = useWorkspaceAccess()
|
|
|
25
25
|
const { open, blockId, instanceId, stepIndex, close } = useResultView('gate')
|
|
26
26
|
const block = computed(() => (blockId.value ? board.getBlock(blockId.value) : undefined))
|
|
27
27
|
const prUrl = computed(() => block.value?.pullRequest?.url ?? null)
|
|
28
|
-
const headerTitle = computed(
|
|
29
|
-
() => `${meta.value.label}${block.value ? ` — ${block.value.title}` : ''}`,
|
|
30
|
-
)
|
|
31
28
|
|
|
32
29
|
const instance = computed(() =>
|
|
33
30
|
instanceId.value === null ? null : (execution.getInstance(instanceId.value) ?? null),
|
|
@@ -42,6 +39,9 @@ const isCi = computed(() => step.value?.agentKind === 'ci')
|
|
|
42
39
|
const isHumanReview = computed(() => step.value?.agentKind === 'human-review')
|
|
43
40
|
const isDocQuality = computed(() => step.value?.agentKind === 'doc-quality')
|
|
44
41
|
const meta = computed(() => agentKindMeta(step.value?.agentKind ?? 'ci'))
|
|
42
|
+
const headerTitle = computed(
|
|
43
|
+
() => `${meta.value.label}${block.value ? ` — ${block.value.title}` : ''}`,
|
|
44
|
+
)
|
|
45
45
|
const helperKind = computed(() =>
|
|
46
46
|
isHumanReview.value
|
|
47
47
|
? 'fixer'
|
|
@@ -189,6 +189,16 @@ function removeSelected(path: string) {
|
|
|
189
189
|
if (i >= 0) selectedDirectories.value.splice(i, 1)
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
+
// The just-added whole-repo service, kept on the board store so the user can configure it
|
|
193
|
+
// (test infra + fragments) right here — the same controls as the inspector. Only the
|
|
194
|
+
// whole-repo flow surfaces this inline configure step; a monorepo adds several services at
|
|
195
|
+
// once and they're configured later in the inspector. Declared above the watcher and
|
|
196
|
+
// `resetSelection` below, both of which clear it.
|
|
197
|
+
const configuredBlockId = ref<string | undefined>(undefined)
|
|
198
|
+
const configuredBlock = computed(() =>
|
|
199
|
+
configuredBlockId.value ? board.getBlock(configuredBlockId.value) : undefined,
|
|
200
|
+
)
|
|
201
|
+
|
|
192
202
|
// On repo change, capture the picked repo (from the volatile loaded list, before a later
|
|
193
203
|
// search replaces it), seed the monorepo toggle from its persisted flag, and clear the rest.
|
|
194
204
|
watch(selectedRepoId, (id) => {
|
|
@@ -227,15 +237,6 @@ function openManageInstall() {
|
|
|
227
237
|
if (manageInstallUrl.value) window.open(manageInstallUrl.value, '_blank', 'noopener')
|
|
228
238
|
}
|
|
229
239
|
|
|
230
|
-
// The just-added whole-repo service, kept on the board store so the user can configure it
|
|
231
|
-
// (test infra + fragments) right here — the same controls as the inspector. Only the
|
|
232
|
-
// whole-repo flow surfaces this inline configure step; a monorepo adds several services at
|
|
233
|
-
// once and they're configured later in the inspector.
|
|
234
|
-
const configuredBlockId = ref<string | undefined>(undefined)
|
|
235
|
-
const configuredBlock = computed(() =>
|
|
236
|
-
configuredBlockId.value ? board.getBlock(configuredBlockId.value) : undefined,
|
|
237
|
-
)
|
|
238
|
-
|
|
239
240
|
// On open: ensure we know the connection + which repos the App can access, and
|
|
240
241
|
// the workspace's already-tracked repos (to flag ones already on the board).
|
|
241
242
|
// Declared after every ref resetSelection() touches so the `immediate` run
|
|
@@ -200,15 +200,10 @@ function openDedicatedWindow() {
|
|
|
200
200
|
else if (park === 'fork-decision') ui.openForkDecision(c.instanceId, c.stepIndex)
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
-
function close() {
|
|
204
|
-
// Reset the approval-mode sub-states so reopening the same step is clean
|
|
205
|
-
// (the step-change watch only fires when the step key actually changes).
|
|
206
|
-
approval.resetForClose()
|
|
207
|
-
ui.closeStepDetail()
|
|
208
|
-
}
|
|
209
|
-
|
|
210
203
|
// The GitHub-style approval/review state machine for a pending gate step. A park a
|
|
211
204
|
// dedicated window owns is NOT reviewable here, so it doesn't count as pending.
|
|
205
|
+
// (`close` is passed by hoisted function reference; it's declared below `approval`,
|
|
206
|
+
// which it resets.)
|
|
212
207
|
const approval = useStepApproval({
|
|
213
208
|
step: () => step.value,
|
|
214
209
|
scrollEl: () => scrollEl.value,
|
|
@@ -246,6 +241,13 @@ const {
|
|
|
246
241
|
reject,
|
|
247
242
|
} = approval
|
|
248
243
|
|
|
244
|
+
function close() {
|
|
245
|
+
// Reset the approval-mode sub-states so reopening the same step is clean
|
|
246
|
+
// (the step-change watch only fires when the step key actually changes).
|
|
247
|
+
approval.resetForClose()
|
|
248
|
+
ui.closeStepDetail()
|
|
249
|
+
}
|
|
250
|
+
|
|
249
251
|
/**
|
|
250
252
|
* Why the gate refuses this viewer, worded for them. An exhaustive Record over the shared refusal
|
|
251
253
|
* vocabulary with LITERAL keys, so the typed-message-key check sees them and a new refusal reason
|
|
@@ -20,10 +20,6 @@ const requirements = useRequirementsStore()
|
|
|
20
20
|
const access = useWorkspaceAccess()
|
|
21
21
|
const { t } = useI18n()
|
|
22
22
|
|
|
23
|
-
// When the selected task block backs a recurring pipeline, the inspector shows the
|
|
24
|
-
// schedule controls + history, and "Delete" removes the schedule (block + history).
|
|
25
|
-
const schedule = computed(() => (block.value ? recurring.byBlock(block.value.id) : undefined))
|
|
26
|
-
|
|
27
23
|
onMounted(() => {
|
|
28
24
|
fragments.ensureLoaded()
|
|
29
25
|
github.ensureLoaded()
|
|
@@ -32,6 +28,10 @@ onMounted(() => {
|
|
|
32
28
|
const block = computed<Block | undefined>(() =>
|
|
33
29
|
ui.selectedBlockId ? board.getBlock(ui.selectedBlockId) : undefined,
|
|
34
30
|
)
|
|
31
|
+
|
|
32
|
+
// When the selected task block backs a recurring pipeline, the inspector shows the
|
|
33
|
+
// schedule controls + history, and "Delete" removes the schedule (block + history).
|
|
34
|
+
const schedule = computed(() => (block.value ? recurring.byBlock(block.value.id) : undefined))
|
|
35
35
|
const level = computed(() => block.value?.level ?? 'frame')
|
|
36
36
|
const isFrame = computed(() => level.value === 'frame')
|
|
37
37
|
|
|
@@ -190,6 +190,18 @@ function rootUnderDirectory(directory: string | null | undefined, path: string):
|
|
|
190
190
|
return segs.join('/')
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
+
// The repo + service subdirectory backing this frame (the manifest prefills, the fixer
|
|
194
|
+
// run, and the compose-file browser all root paths under it). A monorepo service isn't
|
|
195
|
+
// on the `github_repos` blockId link (that stays null), so fall back to the service
|
|
196
|
+
// catalog mapping, which carries the repo + directory.
|
|
197
|
+
const repoContext = computed<{ githubId: number; directory?: string | null } | undefined>(() => {
|
|
198
|
+
if (props.repo) return props.repo
|
|
199
|
+
const svc = services.serviceByFrameBlock[props.block.id]
|
|
200
|
+
if (svc?.repoGithubId != null) return { githubId: svc.repoGithubId, directory: svc.directory }
|
|
201
|
+
const r = github.repoForBlock(props.block.id)
|
|
202
|
+
return r ? { githubId: r.githubId } : undefined
|
|
203
|
+
})
|
|
204
|
+
|
|
193
205
|
function setCustomManifestId(value: string) {
|
|
194
206
|
// Prefill the manifest path with the selected type's default, rooted under the service subtree
|
|
195
207
|
// (repo-root-relative, editable afterwards) so a monorepo service targets the right location
|
|
@@ -366,17 +378,6 @@ async function stopEnvTest() {
|
|
|
366
378
|
// ephemeral-environment provisioner, not commonly tuned — keep them collapsed by default.
|
|
367
379
|
const showProvisioning = ref(false)
|
|
368
380
|
|
|
369
|
-
// The repo + service subdirectory backing this frame, for the compose-file browser.
|
|
370
|
-
// A monorepo service isn't on the `github_repos` blockId link (that stays null), so
|
|
371
|
-
// fall back to the service catalog mapping, which carries the repo + directory.
|
|
372
|
-
const repoContext = computed<{ githubId: number; directory?: string | null } | undefined>(() => {
|
|
373
|
-
if (props.repo) return props.repo
|
|
374
|
-
const svc = services.serviceByFrameBlock[props.block.id]
|
|
375
|
-
if (svc?.repoGithubId != null) return { githubId: svc.repoGithubId, directory: svc.directory }
|
|
376
|
-
const r = github.repoForBlock(props.block.id)
|
|
377
|
-
return r ? { githubId: r.githubId } : undefined
|
|
378
|
-
})
|
|
379
|
-
|
|
380
381
|
// Repo-path picker, shared by the compose file (`docker compose -f <path>`) and the
|
|
381
382
|
// kubernetes colocated manifests path. The stored path is relative to the repo root (the
|
|
382
383
|
// browser starts inside the service's subdirectory for convenience).
|
|
@@ -56,6 +56,13 @@ const instance = computed(() => execution.getInstance(props.block.executionId))
|
|
|
56
56
|
// is that nothing will merge, and WHY is answered by the run's own notes and the merge decision.
|
|
57
57
|
const sandboxed = computed(() => isDryRun(instance.value?.mode))
|
|
58
58
|
|
|
59
|
+
// A failed pipeline run surfaces the shared failure banner + retry — the
|
|
60
|
+
// execution failure surface that the old `pr_ready` flip used to hide.
|
|
61
|
+
const failedRun = computed(() => {
|
|
62
|
+
const run = agentRuns.byBlock[props.block.id]
|
|
63
|
+
return run && run.status === 'failed' ? run : null
|
|
64
|
+
})
|
|
65
|
+
|
|
59
66
|
// Nothing to show yet: no run, no failed run, no PR, and not awaiting a merge — render an
|
|
60
67
|
// empty state instead of a blank gap so the section reads as "no runs yet" rather than broken.
|
|
61
68
|
const isEmpty = computed(
|
|
@@ -77,13 +84,6 @@ const runFailed = computed(() => instance.value?.status === 'failed')
|
|
|
77
84
|
*/
|
|
78
85
|
const inputGateNotice = computed(() => inputGateNoticeFor(instance.value))
|
|
79
86
|
|
|
80
|
-
// A failed pipeline run surfaces the shared failure banner + retry — the
|
|
81
|
-
// execution failure surface that the old `pr_ready` flip used to hide.
|
|
82
|
-
const failedRun = computed(() => {
|
|
83
|
-
const run = agentRuns.byBlock[props.block.id]
|
|
84
|
-
return run && run.status === 'failed' ? run : null
|
|
85
|
-
})
|
|
86
|
-
|
|
87
87
|
// Failures from prior attempts, preserved across retries — shown regardless of the run's
|
|
88
88
|
// CURRENT status, so the error trail stays viewable after a restart clears the top banner.
|
|
89
89
|
const failureHistory = computed(() => agentRuns.byBlock[props.block.id]?.failureHistory ?? [])
|
|
@@ -160,6 +160,11 @@ const STATUS_META = computed<Record<ExecutionInstance['status'], { label: string
|
|
|
160
160
|
const steps = computed(() => props.instance.steps)
|
|
161
161
|
const total = computed(() => steps.value.length)
|
|
162
162
|
|
|
163
|
+
// A failed run is no longer executing: a step left mid-flight (state still `working`,
|
|
164
|
+
// its container caught mid cold-boot) must stop looking live — no spinner, no pulse,
|
|
165
|
+
// no "spinning up container" phase.
|
|
166
|
+
const runFailed = computed(() => props.instance.status === 'failed')
|
|
167
|
+
|
|
163
168
|
// A shared 1s tick drives every step's live elapsed clock, so a step that hasn't yet
|
|
164
169
|
// emitted subtask counts still shows it is progressing rather than reading as hung.
|
|
165
170
|
const nowTick = useNowTick()
|
|
@@ -172,10 +177,6 @@ function stepElapsed(s: PipelineStep): string | null {
|
|
|
172
177
|
// human can see at a glance whether the fixer ran or was skipped.
|
|
173
178
|
const companionByStep = computed(() => steps.value.map((s) => gateCompanionFor(s, runFailed.value)))
|
|
174
179
|
|
|
175
|
-
// A failed run is no longer executing: a step left mid-flight (state still `working`,
|
|
176
|
-
// its container caught mid cold-boot) must stop looking live — no spinner, no pulse,
|
|
177
|
-
// no "spinning up container" phase.
|
|
178
|
-
const runFailed = computed(() => props.instance.status === 'failed')
|
|
179
180
|
/**
|
|
180
181
|
* A reviewer gate (requirements-review / clarity-review) folding the answers or
|
|
181
182
|
* re-reviewing in the durable driver: the step parks in `waiting_decision` but is actively
|
|
@@ -33,6 +33,9 @@ const { confirmAction, toastDone } = useConfirmAction()
|
|
|
33
33
|
/** Account-wide mode (single account scope) vs the default workspace/user toggle. */
|
|
34
34
|
const isAccount = computed(() => !!props.accountId)
|
|
35
35
|
|
|
36
|
+
/** Which store the form writes to: the shared workspace keys or the user's own. */
|
|
37
|
+
const scope = ref<'workspace' | 'user'>('workspace')
|
|
38
|
+
|
|
36
39
|
// "My keys" (user scope) are stored per-user, so they need a signed-in user. Block just
|
|
37
40
|
// that scope when there's none (a deployment without sign-in); workspace/account keys are
|
|
38
41
|
// unaffected. The scope toggle stays enabled so the user can switch back to a shared scope.
|
|
@@ -136,7 +139,6 @@ const PROVIDERS = computed(() =>
|
|
|
136
139
|
)
|
|
137
140
|
const ALL_PROVIDERS = computed(() => [...DIRECT_PROVIDERS.value, ...PROXY_PROVIDERS.value])
|
|
138
141
|
|
|
139
|
-
const scope = ref<'workspace' | 'user'>('workspace')
|
|
140
142
|
const provider = ref<ApiKeyProvider>(props.category === 'proxy' ? 'openrouter' : 'openai')
|
|
141
143
|
const label = ref('')
|
|
142
144
|
const key = ref('')
|
|
@@ -19,9 +19,6 @@ const { t, d } = useI18n()
|
|
|
19
19
|
// overlay behaviour.
|
|
20
20
|
const { open, blockId, instanceId, stepIndex, close } = useResultView('ralph-loop')
|
|
21
21
|
const block = computed(() => (blockId.value ? board.getBlock(blockId.value) : undefined))
|
|
22
|
-
const headerTitle = computed(
|
|
23
|
-
() => `${meta.value.label}${block.value ? ` — ${block.value.title}` : ''}`,
|
|
24
|
-
)
|
|
25
22
|
const prUrl = computed(() => block.value?.pullRequest?.url ?? null)
|
|
26
23
|
|
|
27
24
|
const instance = computed(() =>
|
|
@@ -33,6 +30,9 @@ const step = computed(() => {
|
|
|
33
30
|
})
|
|
34
31
|
const ralph = computed<RalphStepState | null>(() => step.value?.ralph ?? null)
|
|
35
32
|
const meta = computed(() => agentKindMeta('ralph'))
|
|
33
|
+
const headerTitle = computed(
|
|
34
|
+
() => `${meta.value.label}${block.value ? ` — ${block.value.title}` : ''}`,
|
|
35
|
+
)
|
|
36
36
|
|
|
37
37
|
// Iterations, newest-first for the timeline.
|
|
38
38
|
const attempts = computed(() => [...(ralph.value?.attemptLog ?? [])].reverse())
|
|
@@ -131,6 +131,20 @@ function buildFindings(): { text: string; structured: { view?: string; note: str
|
|
|
131
131
|
return { text: blocks.join('\n\n'), structured }
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
// Degraded-basis approval guard (no capture / a fix landed after these shots): require an
|
|
135
|
+
// explicit "I reviewed this another way" acknowledgement before the one-click approve.
|
|
136
|
+
const ackDegraded = ref(false)
|
|
137
|
+
watch(
|
|
138
|
+
() => vc.value?.degradedReason ?? null,
|
|
139
|
+
() => {
|
|
140
|
+
ackDegraded.value = false
|
|
141
|
+
},
|
|
142
|
+
)
|
|
143
|
+
const needsAck = computed(() => !!vc.value?.degradedReason)
|
|
144
|
+
const canApprove = computed(
|
|
145
|
+
() => awaitingHuman.value && !busy.value && (!needsAck.value || ackDegraded.value),
|
|
146
|
+
)
|
|
147
|
+
|
|
134
148
|
async function approve() {
|
|
135
149
|
if (!blockId.value || !canApprove.value) return
|
|
136
150
|
await visualConfirm.approve(blockId.value)
|
|
@@ -169,20 +183,6 @@ async function onFilePicked(e: Event) {
|
|
|
169
183
|
uploadView.value = ''
|
|
170
184
|
if (fileInput.value) fileInput.value.value = ''
|
|
171
185
|
}
|
|
172
|
-
|
|
173
|
-
// Degraded-basis approval guard (no capture / a fix landed after these shots): require an
|
|
174
|
-
// explicit "I reviewed this another way" acknowledgement before the one-click approve.
|
|
175
|
-
const ackDegraded = ref(false)
|
|
176
|
-
watch(
|
|
177
|
-
() => vc.value?.degradedReason ?? null,
|
|
178
|
-
() => {
|
|
179
|
-
ackDegraded.value = false
|
|
180
|
-
},
|
|
181
|
-
)
|
|
182
|
-
const needsAck = computed(() => !!vc.value?.degradedReason)
|
|
183
|
-
const canApprove = computed(
|
|
184
|
-
() => awaitingHuman.value && !busy.value && (!needsAck.value || ackDegraded.value),
|
|
185
|
-
)
|
|
186
186
|
</script>
|
|
187
187
|
|
|
188
188
|
<template>
|
package/app/pages/index.vue
CHANGED
|
@@ -285,6 +285,27 @@ watch(
|
|
|
285
285
|
{ immediate: true },
|
|
286
286
|
)
|
|
287
287
|
|
|
288
|
+
// Probe the GitHub integration as soon as a board is active (re-probe per board —
|
|
289
|
+
// connections are per workspace). The result drives the onboarding gate in the template
|
|
290
|
+
// before the board mounts, so an unconnected user can't slip past it. `ensureProbed`
|
|
291
|
+
// single-flights per board (app-startup initiative, item 12), so this and the SideBar's
|
|
292
|
+
// probe collapse to one request on a cold open instead of two.
|
|
293
|
+
watch(
|
|
294
|
+
() => workspace.workspaceId,
|
|
295
|
+
(id) => {
|
|
296
|
+
if (id) void github.ensureProbed()
|
|
297
|
+
},
|
|
298
|
+
{ immediate: true },
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
// Hard gate: the App is enabled on the backend but this workspace has no
|
|
302
|
+
// installation yet. `available === null` means the probe is still in flight.
|
|
303
|
+
// Both are declared here, ahead of the tutorial offer below, because that offer reads them
|
|
304
|
+
// from a watcher that runs synchronously during setup (`immediate: true`). Declared after
|
|
305
|
+
// it, they'd still be in their TDZ and the first run would throw.
|
|
306
|
+
const needsGitHubInstall = computed(() => github.available === true && !github.connected)
|
|
307
|
+
const githubProbePending = computed(() => github.available === null)
|
|
308
|
+
|
|
288
309
|
// Offer the tutorial on launch, once the board is up. Yields to every other startup
|
|
289
310
|
// surface — the GitHub onboarding gate and the advisory/onboarding modals above — so a
|
|
290
311
|
// first launch never stacks the tour prompt on top of a dialog that needs answering
|
|
@@ -347,24 +368,6 @@ useTutorialNudge()
|
|
|
347
368
|
// simply has nothing to talk to, and the browser-persisted store carries on alone.
|
|
348
369
|
useTutorialSync()
|
|
349
370
|
|
|
350
|
-
// Probe the GitHub integration as soon as a board is active (re-probe per board —
|
|
351
|
-
// connections are per workspace). The result drives the onboarding gate below
|
|
352
|
-
// before the board mounts, so an unconnected user can't slip past it. `ensureProbed`
|
|
353
|
-
// single-flights per board (app-startup initiative, item 12), so this and the SideBar's
|
|
354
|
-
// probe collapse to one request on a cold open instead of two.
|
|
355
|
-
watch(
|
|
356
|
-
() => workspace.workspaceId,
|
|
357
|
-
(id) => {
|
|
358
|
-
if (id) void github.ensureProbed()
|
|
359
|
-
},
|
|
360
|
-
{ immediate: true },
|
|
361
|
-
)
|
|
362
|
-
|
|
363
|
-
// Hard gate: the App is enabled on the backend but this workspace has no
|
|
364
|
-
// installation yet. `available === null` means the probe is still in flight.
|
|
365
|
-
const needsGitHubInstall = computed(() => github.available === true && !github.connected)
|
|
366
|
-
const githubProbePending = computed(() => github.available === null)
|
|
367
|
-
|
|
368
371
|
// Subscribe to the backend's real-time event stream and (re)connect whenever the
|
|
369
372
|
// active workspace changes. Runs advance durably server-side; progress arrives as
|
|
370
373
|
// pushed events rather than by polling.
|
package/app/stores/execution.ts
CHANGED
|
@@ -131,6 +131,12 @@ export const useExecutionStore = defineStore('execution', () => {
|
|
|
131
131
|
} else instances.value.push(instance)
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
const byId = computed(() => {
|
|
135
|
+
const map = new Map<string, ExecutionInstance>()
|
|
136
|
+
for (const e of instances.value) map.set(e.id, e)
|
|
137
|
+
return map
|
|
138
|
+
})
|
|
139
|
+
|
|
134
140
|
/**
|
|
135
141
|
* Run an action that returns a run's authoritative sub-state and apply that state to the cached
|
|
136
142
|
* run as an OPTIMISTIC ECHO — but only when the event stream has not delivered a newer revision
|
|
@@ -169,12 +175,6 @@ export const useExecutionStore = defineStore('execution', () => {
|
|
|
169
175
|
return state
|
|
170
176
|
}
|
|
171
177
|
|
|
172
|
-
const byId = computed(() => {
|
|
173
|
-
const map = new Map<string, ExecutionInstance>()
|
|
174
|
-
for (const e of instances.value) map.set(e.id, e)
|
|
175
|
-
return map
|
|
176
|
-
})
|
|
177
|
-
|
|
178
178
|
function getInstance(id: string | null | undefined) {
|
|
179
179
|
return id ? byId.value.get(id) : undefined
|
|
180
180
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cat-factory/app",
|
|
3
|
-
"version": "0.228.
|
|
3
|
+
"version": "0.228.1",
|
|
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",
|