@cat-factory/app 0.26.5 → 0.26.7
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/AgentFailureCard.vue +2 -7
- package/app/components/board/nodes/TaskCard.vue +5 -15
- package/app/components/settings/ProviderConnectionPanel.vue +3 -1
- package/app/composables/usePipelineErrorToast.ts +100 -0
- package/app/stores/agentRuns.ts +12 -4
- package/app/stores/execution.ts +32 -12
- package/package.json +1 -1
|
@@ -12,7 +12,6 @@ const props = withDefaults(
|
|
|
12
12
|
)
|
|
13
13
|
|
|
14
14
|
const agentRuns = useAgentRunsStore()
|
|
15
|
-
const toast = useToast()
|
|
16
15
|
|
|
17
16
|
const compact = computed(() => props.variant === 'compact')
|
|
18
17
|
const failure = computed(() => props.run.failure)
|
|
@@ -26,13 +25,9 @@ async function retry() {
|
|
|
26
25
|
if (retrying.value) return
|
|
27
26
|
retrying.value = true
|
|
28
27
|
try {
|
|
28
|
+
// The store surfaces any failure as an actionable toast (incl. the no-provider 409),
|
|
29
|
+
// so we only need to clear the in-flight guard here.
|
|
29
30
|
await agentRuns.retry(props.run.runId)
|
|
30
|
-
} catch (e) {
|
|
31
|
-
toast.add({
|
|
32
|
-
title: 'Retry failed',
|
|
33
|
-
description: e instanceof Error ? e.message : String(e),
|
|
34
|
-
color: 'error',
|
|
35
|
-
})
|
|
36
31
|
} finally {
|
|
37
32
|
retrying.value = false
|
|
38
33
|
}
|
|
@@ -77,21 +77,11 @@ async function run() {
|
|
|
77
77
|
return
|
|
78
78
|
}
|
|
79
79
|
starting.value = true
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
} catch (e) {
|
|
86
|
-
// Real confirmation came back as a failure — revert the optimistic state.
|
|
87
|
-
starting.value = false
|
|
88
|
-
toast.add({
|
|
89
|
-
title: 'Failed to start',
|
|
90
|
-
description: e instanceof Error ? e.message : String(e),
|
|
91
|
-
color: 'error',
|
|
92
|
-
icon: 'i-lucide-alert-triangle',
|
|
93
|
-
})
|
|
94
|
-
}
|
|
80
|
+
// false ⇒ the run never started (the user cancelled the personal-password prompt, or
|
|
81
|
+
// the start was refused — the store surfaces the actionable toast itself). Revert the
|
|
82
|
+
// optimistic state; on success the button unmounts once the stream pushes in_progress.
|
|
83
|
+
const started = await execution.start(props.taskId, pipeline)
|
|
84
|
+
if (!started) starting.value = false
|
|
95
85
|
}
|
|
96
86
|
|
|
97
87
|
function review() {
|
|
@@ -114,7 +114,9 @@ function buildManifestPayload(): {
|
|
|
114
114
|
const template = descriptor.value?.manifestTemplate
|
|
115
115
|
if (!template) return null
|
|
116
116
|
const base = descriptor.value?.savedManifest ?? template
|
|
117
|
-
|
|
117
|
+
// `base` is a Vue reactive proxy, which structuredClone refuses (DataCloneError). The
|
|
118
|
+
// manifest is plain JSON config, so a JSON round-trip both unwraps the proxy and deep-clones.
|
|
119
|
+
const manifest: Record<string, unknown> = JSON.parse(JSON.stringify(base))
|
|
118
120
|
const providerConfig: Record<string, unknown> = {
|
|
119
121
|
...(manifest.providerConfig as Record<string, unknown> | undefined),
|
|
120
122
|
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turn a failed run-control API call (start / restart / retry / merge) into an actionable
|
|
3
|
+
* toast. The backend tags every 409 conflict with a distinct, machine-readable
|
|
4
|
+
* `error.details.reason` (kernel `ConflictReason`), so we can word each case precisely
|
|
5
|
+
* instead of dumping the raw message — and, for `providers_unconfigured`, surface the
|
|
6
|
+
* SAME guidance + "Configure AI" jump as the no-AI-provider startup banner.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** The parsed shape of a backend conflict (`{ error: { code: 'conflict', details } }`). */
|
|
10
|
+
interface ConflictDetails {
|
|
11
|
+
reason?: string
|
|
12
|
+
models?: string[]
|
|
13
|
+
[key: string]: unknown
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Pull a 409 conflict's `{ reason, message, details }` out of a thrown fetch error, else null. */
|
|
17
|
+
export function parseConflict(
|
|
18
|
+
error: unknown,
|
|
19
|
+
): { reason?: string; message: string; details: ConflictDetails } | null {
|
|
20
|
+
const body = (
|
|
21
|
+
error as { data?: { error?: { code?: string; message?: string; details?: ConflictDetails } } }
|
|
22
|
+
)?.data?.error
|
|
23
|
+
if (body?.code !== 'conflict') return null
|
|
24
|
+
const details = body.details ?? {}
|
|
25
|
+
return {
|
|
26
|
+
reason: typeof details.reason === 'string' ? details.reason : undefined,
|
|
27
|
+
message: body.message ?? 'This action conflicts with the current state.',
|
|
28
|
+
details,
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Per-reason toast titles for conflicts that don't get bespoke handling below. */
|
|
33
|
+
const CONFLICT_TITLES: Record<string, string> = {
|
|
34
|
+
dependencies_unmet: 'Blocked by dependencies',
|
|
35
|
+
task_limit_reached: 'Concurrency limit reached',
|
|
36
|
+
tester_infra_unsupported: 'Test infrastructure not configured',
|
|
37
|
+
run_not_retryable: 'Run can’t be retried',
|
|
38
|
+
no_pr_to_merge: 'No PR to merge',
|
|
39
|
+
github_not_connected: 'GitHub not connected',
|
|
40
|
+
bootstrap_not_retryable: 'Bootstrap can’t be retried',
|
|
41
|
+
bootstrap_reference_missing: 'Reference architecture is gone',
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function usePipelineErrorToast() {
|
|
45
|
+
const toast = useToast()
|
|
46
|
+
const ui = useUiStore()
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Present `error` as a toast. `fallbackTitle` is used for non-conflict failures and any
|
|
50
|
+
* conflict reason without a dedicated title.
|
|
51
|
+
*/
|
|
52
|
+
function present(error: unknown, fallbackTitle = 'Action failed'): void {
|
|
53
|
+
const conflict = parseConflict(error)
|
|
54
|
+
|
|
55
|
+
// The headline case: a pipeline step's model has no usable provider. Name the
|
|
56
|
+
// offending model(s), explain no provider is available, and offer the one-click jump
|
|
57
|
+
// to the AI setup — the same remedy the startup "No AI model configured" banner gives.
|
|
58
|
+
if (conflict?.reason === 'providers_unconfigured') {
|
|
59
|
+
const models = Array.isArray(conflict.details.models) ? conflict.details.models : []
|
|
60
|
+
const list = models.join(', ')
|
|
61
|
+
toast.add({
|
|
62
|
+
title: 'No AI provider for this model',
|
|
63
|
+
description: list
|
|
64
|
+
? `No provider is configured for ${list}. Add a provider key, connect a subscription, ` +
|
|
65
|
+
'or enable Cloudflare AI to run it.'
|
|
66
|
+
: conflict.message,
|
|
67
|
+
color: 'error',
|
|
68
|
+
icon: 'i-lucide-cpu',
|
|
69
|
+
actions: [
|
|
70
|
+
{
|
|
71
|
+
label: 'Configure AI',
|
|
72
|
+
icon: 'i-lucide-settings',
|
|
73
|
+
onClick: () => ui.openAiProviderSetup(),
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
})
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (conflict) {
|
|
81
|
+
toast.add({
|
|
82
|
+
title: CONFLICT_TITLES[conflict.reason ?? ''] ?? fallbackTitle,
|
|
83
|
+
description: conflict.message,
|
|
84
|
+
color: 'warning',
|
|
85
|
+
icon: 'i-lucide-triangle-alert',
|
|
86
|
+
})
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Not a conflict (a 4xx/5xx or a network fault) — surface its message plainly.
|
|
91
|
+
toast.add({
|
|
92
|
+
title: fallbackTitle,
|
|
93
|
+
description: error instanceof Error ? error.message : String(error),
|
|
94
|
+
color: 'error',
|
|
95
|
+
icon: 'i-lucide-triangle-alert',
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return { present }
|
|
100
|
+
}
|
package/app/stores/agentRuns.ts
CHANGED
|
@@ -37,6 +37,10 @@ export interface AgentRunSummary {
|
|
|
37
37
|
export const useAgentRunsStore = defineStore('agentRuns', () => {
|
|
38
38
|
const api = useApi()
|
|
39
39
|
const execution = useExecutionStore()
|
|
40
|
+
// Same actionable-toast handling as the execution store: a retry refused with a tagged
|
|
41
|
+
// 409 (e.g. the run is no longer in a retryable state, or the model has no provider) is
|
|
42
|
+
// surfaced here so every retry surface (board card, inspector, task panel) is identical.
|
|
43
|
+
const runErrors = usePipelineErrorToast()
|
|
40
44
|
|
|
41
45
|
/** Bootstrap runs for this workspace, newest-first. */
|
|
42
46
|
const bootstrapJobs = ref<BootstrapJob[]>([])
|
|
@@ -99,10 +103,14 @@ export const useAgentRunsStore = defineStore('agentRuns', () => {
|
|
|
99
103
|
const personal = usePersonalSubscriptionsStore()
|
|
100
104
|
// A failed run on a Claude-pinned block needs the retrying user's personal password;
|
|
101
105
|
// supplied from cache and prompted (then retried) on a 428, exactly like start.
|
|
102
|
-
|
|
103
|
-
await
|
|
104
|
-
|
|
105
|
-
|
|
106
|
+
try {
|
|
107
|
+
await personal.withCredential(async (password) => {
|
|
108
|
+
await api.retryAgentRun(ws.requireId(), runId, password)
|
|
109
|
+
await ws.refresh()
|
|
110
|
+
})
|
|
111
|
+
} catch (e) {
|
|
112
|
+
runErrors.present(e, 'Retry failed')
|
|
113
|
+
}
|
|
106
114
|
}
|
|
107
115
|
|
|
108
116
|
/**
|
package/app/stores/execution.ts
CHANGED
|
@@ -18,6 +18,11 @@ import { useWorkspaceStore } from '~/stores/workspace'
|
|
|
18
18
|
*/
|
|
19
19
|
export const useExecutionStore = defineStore('execution', () => {
|
|
20
20
|
const api = useApi()
|
|
21
|
+
// Centralised actionable toasts for run-control failures: a 409 with no configured
|
|
22
|
+
// provider opens the AI setup; the other tagged conflicts get worded titles. Living
|
|
23
|
+
// in the store means every caller (board card, drag-drop, menus, restart controls)
|
|
24
|
+
// gets identical handling, including the fire-and-forget ones that never caught.
|
|
25
|
+
const runErrors = usePipelineErrorToast()
|
|
21
26
|
const instances = ref<ExecutionInstance[]>([])
|
|
22
27
|
|
|
23
28
|
/** Replace the cached executions with a server snapshot. */
|
|
@@ -109,12 +114,18 @@ export const useExecutionStore = defineStore('execution', () => {
|
|
|
109
114
|
async function start(blockId: string, pipeline: Pipeline): Promise<boolean> {
|
|
110
115
|
const ws = useWorkspaceStore()
|
|
111
116
|
const personal = usePersonalSubscriptionsStore()
|
|
112
|
-
// Returns false when the user cancels the personal-password prompt
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
await
|
|
117
|
-
|
|
117
|
+
// Returns false when the user cancels the personal-password prompt OR the start was
|
|
118
|
+
// refused (a 409 conflict, surfaced as an actionable toast here), so an optimistic
|
|
119
|
+
// caller can revert its "Starting…" state without its own error handling.
|
|
120
|
+
try {
|
|
121
|
+
return await personal.withCredential(async (password) => {
|
|
122
|
+
await api.startExecution(ws.requireId(), blockId, { pipelineId: pipeline.id }, password)
|
|
123
|
+
await ws.refresh()
|
|
124
|
+
})
|
|
125
|
+
} catch (e) {
|
|
126
|
+
runErrors.present(e, 'Failed to start')
|
|
127
|
+
return false
|
|
128
|
+
}
|
|
118
129
|
}
|
|
119
130
|
|
|
120
131
|
// Interacting with a running individual-usage run (resolve/approve/request-changes) rides
|
|
@@ -207,8 +218,12 @@ export const useExecutionStore = defineStore('execution', () => {
|
|
|
207
218
|
/** Merge an open PR (a task in `pr_ready`) — the server completes the task. */
|
|
208
219
|
async function mergePr(blockId: string) {
|
|
209
220
|
const ws = useWorkspaceStore()
|
|
210
|
-
|
|
211
|
-
|
|
221
|
+
try {
|
|
222
|
+
await api.mergeBlock(ws.requireId(), blockId)
|
|
223
|
+
await ws.refresh()
|
|
224
|
+
} catch (e) {
|
|
225
|
+
runErrors.present(e, 'Failed to merge')
|
|
226
|
+
}
|
|
212
227
|
}
|
|
213
228
|
|
|
214
229
|
/**
|
|
@@ -222,10 +237,15 @@ export const useExecutionStore = defineStore('execution', () => {
|
|
|
222
237
|
async function restartFromStep(instanceId: string, stepIndex: number): Promise<boolean> {
|
|
223
238
|
const ws = useWorkspaceStore()
|
|
224
239
|
const personal = usePersonalSubscriptionsStore()
|
|
225
|
-
|
|
226
|
-
await
|
|
227
|
-
|
|
228
|
-
|
|
240
|
+
try {
|
|
241
|
+
return await personal.withCredential(async (password) => {
|
|
242
|
+
await api.restartFromStep(ws.requireId(), instanceId, stepIndex, password)
|
|
243
|
+
await ws.refresh()
|
|
244
|
+
})
|
|
245
|
+
} catch (e) {
|
|
246
|
+
runErrors.present(e, 'Failed to restart')
|
|
247
|
+
return false
|
|
248
|
+
}
|
|
229
249
|
}
|
|
230
250
|
|
|
231
251
|
/** Cancel the execution running against a block and reset it to planned. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cat-factory/app",
|
|
3
|
-
"version": "0.26.
|
|
3
|
+
"version": "0.26.7",
|
|
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",
|