@cat-factory/app 0.150.0 → 0.151.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/AddTaskModal.vue +43 -2
- package/app/components/board/ReviewDebtBadge.vue +26 -0
- package/app/components/board/ReviewFrictionDialog.vue +118 -0
- package/app/components/panels/inspector/ContainerSummary.vue +7 -3
- package/app/components/settings/KubernetesEnvironmentForm.vue +45 -29
- package/app/components/settings/WorkspaceSettingsPanel.vue +118 -1
- package/app/composables/api/board.ts +1 -0
- package/app/composables/usePipelineErrorToast.ts +218 -175
- package/app/composables/useReviewDebt.ts +23 -0
- package/app/pages/index.vue +2 -0
- package/app/stores/board/mutations.ts +4 -0
- package/app/stores/ui/modals.ts +40 -0
- package/app/stores/workspaceSettings.ts +4 -0
- package/app/types/domain.ts +1 -0
- package/i18n/locales/de.json +33 -0
- package/i18n/locales/en.json +33 -0
- package/i18n/locales/es.json +33 -0
- package/i18n/locales/fr.json +33 -0
- package/i18n/locales/he.json +33 -0
- package/i18n/locales/it.json +33 -0
- package/i18n/locales/ja.json +33 -0
- package/i18n/locales/pl.json +33 -0
- package/i18n/locales/tr.json +33 -0
- package/i18n/locales/uk.json +33 -0
- package/package.json +2 -2
|
@@ -166,6 +166,18 @@ const CONFLICT_INFO: Record<Exclude<ConflictReason, BespokeConflictReason>, Conf
|
|
|
166
166
|
run: (ui) => ui.openGitHub(),
|
|
167
167
|
},
|
|
168
168
|
},
|
|
169
|
+
// Opt-in review-debt friction. In the normal task-create flow AddTaskModal intercepts these
|
|
170
|
+
// 409s and opens the friction dialog (which can retry with an acknowledgement), so these entries
|
|
171
|
+
// are the last-resort toast fallback for any OTHER caller — a generic, param-free title +
|
|
172
|
+
// description reusing the dialog's own `errors.reviewFriction.*` namespace.
|
|
173
|
+
review_debt_warn: {
|
|
174
|
+
titleKey: 'errors.reviewFriction.warnTitle',
|
|
175
|
+
descriptionKey: 'errors.reviewFriction.warnToast',
|
|
176
|
+
},
|
|
177
|
+
review_debt_blocked: {
|
|
178
|
+
titleKey: 'errors.reviewFriction.blockedTitle',
|
|
179
|
+
descriptionKey: 'errors.reviewFriction.blockedToast',
|
|
180
|
+
},
|
|
169
181
|
}
|
|
170
182
|
|
|
171
183
|
/**
|
|
@@ -186,200 +198,231 @@ export function parseConflict(
|
|
|
186
198
|
}
|
|
187
199
|
}
|
|
188
200
|
|
|
201
|
+
/** The non-null parsed shape of a backend conflict, as returned by {@link parseConflict}. */
|
|
202
|
+
type ParsedConflict = NonNullable<ReturnType<typeof parseConflict>>
|
|
203
|
+
|
|
189
204
|
export function usePipelineErrorToast() {
|
|
190
205
|
const toast = useToast()
|
|
191
206
|
const ui = useUiStore()
|
|
192
207
|
const { t, te } = useI18n()
|
|
193
208
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const
|
|
209
|
+
// The headline case: a pipeline step's model has no usable provider. Name the
|
|
210
|
+
// offending model(s), explain no provider is available, and offer the one-click jump
|
|
211
|
+
// to the AI setup — the same remedy the startup "No AI model configured" banner gives.
|
|
212
|
+
function presentProvidersUnconfigured(conflict: ParsedConflict): void {
|
|
213
|
+
const models = Array.isArray(conflict.details.models) ? conflict.details.models : []
|
|
214
|
+
const list = models.join(', ')
|
|
215
|
+
toast.add({
|
|
216
|
+
title: t('errors.conflict.providersUnconfigured.title'),
|
|
217
|
+
description: list
|
|
218
|
+
? t('errors.conflict.providersUnconfigured.body', { models: list })
|
|
219
|
+
: (conflict.message ?? t('errors.conflict.fallbackMessage')),
|
|
220
|
+
color: 'error',
|
|
221
|
+
icon: 'i-lucide-cpu',
|
|
222
|
+
// Stay until dismissed: an actionable toast whose remedy button vanishes on the ~5s
|
|
223
|
+
// auto-dismiss takes the one-click fix with it before the user can reach it.
|
|
224
|
+
duration: 0,
|
|
225
|
+
actions: [
|
|
226
|
+
{
|
|
227
|
+
label: t('errors.conflict.providersUnconfigured.action'),
|
|
228
|
+
icon: 'i-lucide-settings',
|
|
229
|
+
onClick: () => ui.openAiProviderSetup(),
|
|
230
|
+
},
|
|
231
|
+
],
|
|
232
|
+
})
|
|
233
|
+
}
|
|
200
234
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
}
|
|
235
|
+
// A pipeline step relies on binary-artifact storage (the UI Tester uploads screenshots)
|
|
236
|
+
// but the account has none configured. Explain it and offer the jump to the content-storage
|
|
237
|
+
// settings — the same shape as the providers-unconfigured case above. Prefer the localized
|
|
238
|
+
// body (it carries no runtime interpolation) so non-English users see translated copy; the
|
|
239
|
+
// raw backend prose is only the last-resort fallback when the locale lacks the key.
|
|
240
|
+
function presentBinaryStorageUnconfigured(conflict: ParsedConflict): void {
|
|
241
|
+
toast.add({
|
|
242
|
+
title: t('errors.conflict.binaryStorageUnconfigured.title'),
|
|
243
|
+
description: te('errors.conflict.binaryStorageUnconfigured.body')
|
|
244
|
+
? t('errors.conflict.binaryStorageUnconfigured.body')
|
|
245
|
+
: (conflict.message ?? t('errors.conflict.fallbackMessage')),
|
|
246
|
+
color: 'error',
|
|
247
|
+
icon: 'i-lucide-image',
|
|
248
|
+
// Sticky, like the providers-unconfigured toast above: keep the "Configure storage"
|
|
249
|
+
// remedy reachable instead of letting it auto-dismiss.
|
|
250
|
+
duration: 0,
|
|
251
|
+
actions: [
|
|
252
|
+
{
|
|
253
|
+
label: t('errors.conflict.binaryStorageUnconfigured.action'),
|
|
254
|
+
icon: 'i-lucide-settings',
|
|
255
|
+
onClick: () => ui.openContentStorageSettings(),
|
|
256
|
+
},
|
|
257
|
+
],
|
|
258
|
+
})
|
|
259
|
+
}
|
|
227
260
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
261
|
+
// A pipeline includes a Deployer, but the SERVICE's ephemeral-environment config (the in-repo
|
|
262
|
+
// "what/where") is incomplete for its declared type. Steer the user straight to THAT service's
|
|
263
|
+
// environment config — the compose wizard for docker-compose, the service inspector otherwise —
|
|
264
|
+
// falling back to the workspace infrastructure window if the frame id wasn't carried.
|
|
265
|
+
function presentDeployerServiceConfig(conflict: ParsedConflict): void {
|
|
266
|
+
const frameId =
|
|
267
|
+
typeof conflict.details.frameId === 'string' ? conflict.details.frameId : undefined
|
|
268
|
+
const provisionType =
|
|
269
|
+
typeof conflict.details.provisionType === 'string'
|
|
270
|
+
? conflict.details.provisionType
|
|
271
|
+
: undefined
|
|
272
|
+
const missing = Array.isArray(conflict.details.missing)
|
|
273
|
+
? conflict.details.missing.join(', ')
|
|
274
|
+
: ''
|
|
275
|
+
toast.add({
|
|
276
|
+
title: t('errors.conflict.deployerServiceConfig.title'),
|
|
277
|
+
description: missing
|
|
278
|
+
? t('errors.conflict.deployerServiceConfig.body', { missing })
|
|
279
|
+
: (conflict.message ?? t('errors.conflict.fallbackMessage')),
|
|
280
|
+
color: 'error',
|
|
281
|
+
icon: 'i-lucide-server',
|
|
282
|
+
// Sticky, like the other actionable conflicts: keep the "Fix configuration" jump reachable.
|
|
283
|
+
duration: 0,
|
|
284
|
+
actions: [
|
|
285
|
+
{
|
|
286
|
+
label: t('errors.conflict.deployerServiceConfig.action'),
|
|
287
|
+
icon: 'i-lucide-settings',
|
|
288
|
+
onClick: () => {
|
|
289
|
+
if (frameId && provisionType === 'docker-compose') ui.openEnvironmentSetup(frameId)
|
|
290
|
+
else if (frameId) ui.select(frameId)
|
|
291
|
+
else ui.openProviderConnection('environment')
|
|
249
292
|
},
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
293
|
+
},
|
|
294
|
+
],
|
|
295
|
+
})
|
|
296
|
+
}
|
|
254
297
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
: (
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
actions: [
|
|
279
|
-
{
|
|
280
|
-
label: t('errors.conflict.deployerServiceConfig.action'),
|
|
281
|
-
icon: 'i-lucide-settings',
|
|
282
|
-
onClick: () => {
|
|
283
|
-
if (frameId && provisionType === 'docker-compose') ui.openEnvironmentSetup(frameId)
|
|
284
|
-
else if (frameId) ui.select(frameId)
|
|
285
|
-
else ui.openProviderConnection('environment')
|
|
286
|
-
},
|
|
287
|
-
},
|
|
288
|
-
],
|
|
289
|
-
})
|
|
290
|
-
return
|
|
291
|
-
}
|
|
298
|
+
// A pipeline includes a Deployer and the service config is sound, but no WORKSPACE handler
|
|
299
|
+
// resolves for the service's provision type (missing or ambiguous). Steer to the Infrastructure
|
|
300
|
+
// window's Test-environments tab. (Also raised by the Tester start gate — same fix applies.)
|
|
301
|
+
function presentProvisionTypeUnhandled(conflict: ParsedConflict): void {
|
|
302
|
+
const type =
|
|
303
|
+
typeof conflict.details.provisionType === 'string' ? conflict.details.provisionType : ''
|
|
304
|
+
toast.add({
|
|
305
|
+
title: t('errors.conflict.provisionTypeUnhandled.title'),
|
|
306
|
+
description: type
|
|
307
|
+
? t('errors.conflict.provisionTypeUnhandled.body', { type })
|
|
308
|
+
: (conflict.message ?? t('errors.conflict.fallbackMessage')),
|
|
309
|
+
color: 'error',
|
|
310
|
+
icon: 'i-lucide-server-cog',
|
|
311
|
+
duration: 0,
|
|
312
|
+
actions: [
|
|
313
|
+
{
|
|
314
|
+
label: t('errors.conflict.provisionTypeUnhandled.action'),
|
|
315
|
+
icon: 'i-lucide-settings',
|
|
316
|
+
onClick: () => ui.openProviderConnection('environment'),
|
|
317
|
+
},
|
|
318
|
+
],
|
|
319
|
+
})
|
|
320
|
+
}
|
|
292
321
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
322
|
+
// A pipeline includes a Deployer, the config is structurally complete, but the live connection
|
|
323
|
+
// probe of the resolved deployment integration failed (unreachable endpoint / apiserver, bad
|
|
324
|
+
// token). Surface the provider's failure detail and steer to the handler to fix + re-test it.
|
|
325
|
+
function presentDeployerConnectionFailed(conflict: ParsedConflict): void {
|
|
326
|
+
const detail = typeof conflict.details.detail === 'string' ? conflict.details.detail : undefined
|
|
327
|
+
toast.add({
|
|
328
|
+
title: t('errors.conflict.deployerConnectionFailed.title'),
|
|
329
|
+
description: detail
|
|
330
|
+
? t('errors.conflict.deployerConnectionFailed.body', { detail })
|
|
331
|
+
: (conflict.message ?? t('errors.conflict.fallbackMessage')),
|
|
332
|
+
color: 'error',
|
|
333
|
+
icon: 'i-lucide-plug',
|
|
334
|
+
duration: 0,
|
|
335
|
+
actions: [
|
|
336
|
+
{
|
|
337
|
+
label: t('errors.conflict.deployerConnectionFailed.action'),
|
|
338
|
+
icon: 'i-lucide-settings',
|
|
339
|
+
onClick: () => ui.openProviderConnection('environment'),
|
|
340
|
+
},
|
|
341
|
+
],
|
|
342
|
+
})
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Dispatch the bespoke conflict reasons (a runtime-interpolated body + a "configure X" action,
|
|
347
|
+
* each with its own key namespace — the ones excluded from `CONFLICT_INFO`). Returns `true` when
|
|
348
|
+
* the reason was one of them (and the toast was raised), `false` to fall through to the generic
|
|
349
|
+
* map. The reason values are mutually exclusive, so dispatch order is irrelevant.
|
|
350
|
+
*/
|
|
351
|
+
function presentBespokeConflict(conflict: ParsedConflict): boolean {
|
|
352
|
+
switch (conflict.reason) {
|
|
353
|
+
case 'providers_unconfigured':
|
|
354
|
+
presentProvidersUnconfigured(conflict)
|
|
355
|
+
return true
|
|
356
|
+
case 'binary_storage_unconfigured':
|
|
357
|
+
presentBinaryStorageUnconfigured(conflict)
|
|
358
|
+
return true
|
|
359
|
+
case 'deployer_service_provisioning_incomplete':
|
|
360
|
+
presentDeployerServiceConfig(conflict)
|
|
361
|
+
return true
|
|
362
|
+
case 'provision_type_unhandled':
|
|
363
|
+
presentProvisionTypeUnhandled(conflict)
|
|
364
|
+
return true
|
|
365
|
+
case 'deployer_connection_test_failed':
|
|
366
|
+
presentDeployerConnectionFailed(conflict)
|
|
367
|
+
return true
|
|
368
|
+
default:
|
|
369
|
+
return false
|
|
316
370
|
}
|
|
371
|
+
}
|
|
317
372
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
373
|
+
/**
|
|
374
|
+
* Per-reason copy from the exhaustive map: a translated title + description, and a jump
|
|
375
|
+
* action for the reasons a UI panel can fix. `te` (translation-exists) guards every lookup,
|
|
376
|
+
* so a key missing from the active locale falls back rather than leaking a raw key: the
|
|
377
|
+
* title falls to the caller's key, the description to the raw backend `message`. An unknown
|
|
378
|
+
* reason (not in the map) gets the same generic title + raw-message fallback.
|
|
379
|
+
*/
|
|
380
|
+
function presentMappedConflict(conflict: ParsedConflict, fallbackTitleKey: string): void {
|
|
381
|
+
const info = conflict.reason
|
|
382
|
+
? CONFLICT_INFO[conflict.reason as Exclude<ConflictReason, BespokeConflictReason>]
|
|
383
|
+
: undefined
|
|
384
|
+
if (info) {
|
|
324
385
|
toast.add({
|
|
325
|
-
title: t(
|
|
326
|
-
description:
|
|
327
|
-
? t(
|
|
386
|
+
title: te(info.titleKey) ? t(info.titleKey) : t(fallbackTitleKey),
|
|
387
|
+
description: te(info.descriptionKey)
|
|
388
|
+
? t(info.descriptionKey)
|
|
328
389
|
: (conflict.message ?? t('errors.conflict.fallbackMessage')),
|
|
329
|
-
color: '
|
|
330
|
-
icon: 'i-lucide-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
390
|
+
color: 'warning',
|
|
391
|
+
icon: 'i-lucide-triangle-alert',
|
|
392
|
+
// A reason with a jump action becomes an actionable, sticky toast (like the bespoke
|
|
393
|
+
// conflicts above) so the one-click remedy doesn't auto-dismiss before it's reached.
|
|
394
|
+
...(info.action
|
|
395
|
+
? {
|
|
396
|
+
duration: 0,
|
|
397
|
+
actions: [
|
|
398
|
+
{
|
|
399
|
+
label: t(info.action.labelKey),
|
|
400
|
+
icon: info.action.icon,
|
|
401
|
+
onClick: () => info.action?.run(ui),
|
|
402
|
+
},
|
|
403
|
+
],
|
|
404
|
+
}
|
|
405
|
+
: {}),
|
|
339
406
|
})
|
|
340
407
|
return
|
|
341
408
|
}
|
|
409
|
+
toast.add({
|
|
410
|
+
title: t(fallbackTitleKey),
|
|
411
|
+
description: conflict.message ?? t('errors.conflict.fallbackMessage'),
|
|
412
|
+
color: 'warning',
|
|
413
|
+
icon: 'i-lucide-triangle-alert',
|
|
414
|
+
})
|
|
415
|
+
}
|
|
342
416
|
|
|
417
|
+
/**
|
|
418
|
+
* Present `error` as a toast. `fallbackTitleKey` is an i18n message key used for
|
|
419
|
+
* non-conflict failures and any conflict reason without a dedicated title.
|
|
420
|
+
*/
|
|
421
|
+
function present(error: unknown, fallbackTitleKey = 'common.actionFailed'): void {
|
|
422
|
+
const conflict = parseConflict(error)
|
|
343
423
|
if (conflict) {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
// so a key missing from the active locale falls back rather than leaking a raw key: the
|
|
347
|
-
// title falls to the caller's key, the description to the raw backend `message`. An unknown
|
|
348
|
-
// reason (not in the map) gets the same generic title + raw-message fallback.
|
|
349
|
-
const info = conflict.reason
|
|
350
|
-
? CONFLICT_INFO[conflict.reason as Exclude<ConflictReason, BespokeConflictReason>]
|
|
351
|
-
: undefined
|
|
352
|
-
if (info) {
|
|
353
|
-
toast.add({
|
|
354
|
-
title: te(info.titleKey) ? t(info.titleKey) : t(fallbackTitleKey),
|
|
355
|
-
description: te(info.descriptionKey)
|
|
356
|
-
? t(info.descriptionKey)
|
|
357
|
-
: (conflict.message ?? t('errors.conflict.fallbackMessage')),
|
|
358
|
-
color: 'warning',
|
|
359
|
-
icon: 'i-lucide-triangle-alert',
|
|
360
|
-
// A reason with a jump action becomes an actionable, sticky toast (like the bespoke
|
|
361
|
-
// conflicts above) so the one-click remedy doesn't auto-dismiss before it's reached.
|
|
362
|
-
...(info.action
|
|
363
|
-
? {
|
|
364
|
-
duration: 0,
|
|
365
|
-
actions: [
|
|
366
|
-
{
|
|
367
|
-
label: t(info.action.labelKey),
|
|
368
|
-
icon: info.action.icon,
|
|
369
|
-
onClick: () => info.action?.run(ui),
|
|
370
|
-
},
|
|
371
|
-
],
|
|
372
|
-
}
|
|
373
|
-
: {}),
|
|
374
|
-
})
|
|
375
|
-
return
|
|
376
|
-
}
|
|
377
|
-
toast.add({
|
|
378
|
-
title: t(fallbackTitleKey),
|
|
379
|
-
description: conflict.message ?? t('errors.conflict.fallbackMessage'),
|
|
380
|
-
color: 'warning',
|
|
381
|
-
icon: 'i-lucide-triangle-alert',
|
|
382
|
-
})
|
|
424
|
+
if (presentBespokeConflict(conflict)) return
|
|
425
|
+
presentMappedConflict(conflict, fallbackTitleKey)
|
|
383
426
|
return
|
|
384
427
|
}
|
|
385
428
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { computed } from 'vue'
|
|
2
|
+
import { assessReviewFriction } from '@cat-factory/contracts'
|
|
3
|
+
import { useNotificationsStore } from '~/stores/notifications'
|
|
4
|
+
import { useWorkspaceSettingsStore } from '~/stores/workspaceSettings'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The client-side review-debt verdict, computed from the SAME pure `assessReviewFriction` the
|
|
8
|
+
* backend enforces with, over the workspace snapshot's open notifications + settings. Lets the
|
|
9
|
+
* add-task affordances pre-warn (a debt badge) BEFORE a create is attempted, so the friction
|
|
10
|
+
* dialog is rarely a surprise. The server stays the authority — this is a hint only. See
|
|
11
|
+
* backend/docs/review-debt-friction.md.
|
|
12
|
+
*/
|
|
13
|
+
export function useReviewDebt() {
|
|
14
|
+
const notifications = useNotificationsStore()
|
|
15
|
+
const settings = useWorkspaceSettingsStore()
|
|
16
|
+
const verdict = computed(() =>
|
|
17
|
+
assessReviewFriction(notifications.open, settings.settings, Date.now()),
|
|
18
|
+
)
|
|
19
|
+
const active = computed(() => verdict.value.kind !== 'ok')
|
|
20
|
+
const debtCount = computed(() => (verdict.value.kind === 'ok' ? 0 : verdict.value.debt.length))
|
|
21
|
+
const blocked = computed(() => verdict.value.kind === 'block')
|
|
22
|
+
return { verdict, active, debtCount, blocked }
|
|
23
|
+
}
|
package/app/pages/index.vue
CHANGED
|
@@ -17,6 +17,7 @@ import DecisionModal from '~/components/panels/DecisionModal.vue'
|
|
|
17
17
|
import AgentStepDetail from '~/components/panels/AgentStepDetail.vue'
|
|
18
18
|
import StepResultViewHost from '~/components/panels/StepResultViewHost.vue'
|
|
19
19
|
import AddTaskModal from '~/components/board/AddTaskModal.vue'
|
|
20
|
+
import ReviewFrictionDialog from '~/components/board/ReviewFrictionDialog.vue'
|
|
20
21
|
import CreateInitiativeModal from '~/components/board/CreateInitiativeModal.vue'
|
|
21
22
|
import GitHubOnboarding from '~/components/github/GitHubOnboarding.vue'
|
|
22
23
|
import CommandBar from '~/components/layout/CommandBar.vue'
|
|
@@ -377,6 +378,7 @@ watch(
|
|
|
377
378
|
<AgentStepDetail />
|
|
378
379
|
<StepResultViewHost />
|
|
379
380
|
<AddTaskModal />
|
|
381
|
+
<ReviewFrictionDialog v-if="ui.reviewFrictionContext" />
|
|
380
382
|
<CreateInitiativeModal />
|
|
381
383
|
<CommandBar />
|
|
382
384
|
<PersonalCredentialModal />
|
|
@@ -73,6 +73,9 @@ export function createBoardMutations(ctx: BoardWriteContext) {
|
|
|
73
73
|
agentConfig?: Record<string, string>
|
|
74
74
|
fragmentIds?: string[]
|
|
75
75
|
technical?: boolean
|
|
76
|
+
// Opt-in review-debt friction: set on the retry after the human confirms the friction
|
|
77
|
+
// dialog, so a soft `review_debt_warn` 409 is tunnelled through (never a hard block).
|
|
78
|
+
acknowledgeReviewDebt?: boolean
|
|
76
79
|
},
|
|
77
80
|
): Promise<Block | undefined> {
|
|
78
81
|
if (!getBlock(containerId)) return
|
|
@@ -90,6 +93,7 @@ export function createBoardMutations(ctx: BoardWriteContext) {
|
|
|
90
93
|
// when a caller doesn't manage fragments at all (then the backend seeds from the service).
|
|
91
94
|
...(options?.fragmentIds !== undefined ? { fragmentIds: options.fragmentIds } : {}),
|
|
92
95
|
...(options?.technical ? { technical: true } : {}),
|
|
96
|
+
...(options?.acknowledgeReviewDebt ? { acknowledgeReviewDebt: true } : {}),
|
|
93
97
|
})
|
|
94
98
|
upsert(block)
|
|
95
99
|
return block
|
package/app/stores/ui/modals.ts
CHANGED
|
@@ -25,6 +25,29 @@ export interface K3sSetupPrefill {
|
|
|
25
25
|
insecureSkipTlsVerify?: boolean
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/** One task waiting on human review, as carried on a review-debt friction 409's details. */
|
|
29
|
+
export interface ReviewDebtRow {
|
|
30
|
+
blockId: string
|
|
31
|
+
/** The task's title, joined in server-side; null when it couldn't be resolved. */
|
|
32
|
+
title: string | null
|
|
33
|
+
/** How long the task has been waiting on review, in minutes. */
|
|
34
|
+
waitingMinutes: number
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The context a review-debt friction dialog renders + acts on (from the parsed 409). */
|
|
38
|
+
export interface ReviewFrictionModalContext {
|
|
39
|
+
/** `warn` = soft friction (the human may proceed); `blocked` = hard block (they may not). */
|
|
40
|
+
kind: 'warn' | 'blocked'
|
|
41
|
+
/** Which hard trigger fired (`count` / `stuck`), for the blocked-tier message. */
|
|
42
|
+
reason?: 'count' | 'stuck'
|
|
43
|
+
/** The count / stuck-minutes threshold that fired, for the message. */
|
|
44
|
+
threshold?: number | null
|
|
45
|
+
/** The waiting tasks, worst-first, each deep-linkable to its block. */
|
|
46
|
+
debt: ReviewDebtRow[]
|
|
47
|
+
/** Retry the create with `acknowledgeReviewDebt` — present only for the soft `warn` tier. */
|
|
48
|
+
onConfirm: (() => void) | null
|
|
49
|
+
}
|
|
50
|
+
|
|
28
51
|
/** Clears both hub came-from markers; injected into the slices whose `open*` handlers reset them. */
|
|
29
52
|
type ResetHubReturn = () => void
|
|
30
53
|
|
|
@@ -167,6 +190,14 @@ function createDocumentTaskModals(resetHubReturn: ResetHubReturn) {
|
|
|
167
190
|
// linked context). The user still confirms pipeline / preset before adding.
|
|
168
191
|
const addTaskPrefill = ref<AddTaskPrefill | null>(null)
|
|
169
192
|
|
|
193
|
+
// Review-debt friction dialog: opened when a task-create request is refused by the opt-in
|
|
194
|
+
// friction gate (`review_debt_warn` / `review_debt_blocked` 409). Carries the parsed conflict
|
|
195
|
+
// details the backend supplied (the waiting tasks + the trigger) so the dialog can list exactly
|
|
196
|
+
// what is in review, and — for the soft `warn` tier — an `onConfirm` the "Create anyway" button
|
|
197
|
+
// runs to retry the create with `acknowledgeReviewDebt`. Null when closed. See
|
|
198
|
+
// backend/docs/review-debt-friction.md.
|
|
199
|
+
const reviewFrictionContext = ref<ReviewFrictionModalContext | null>(null)
|
|
200
|
+
|
|
170
201
|
// Add-recurring-pipeline modal: the service frame a new recurring pipeline is
|
|
171
202
|
// being added to, or null when closed (mirrors the add-task flow — a button on
|
|
172
203
|
// the frame opens it, scoped to that frame).
|
|
@@ -232,6 +263,12 @@ function createDocumentTaskModals(resetHubReturn: ResetHubReturn) {
|
|
|
232
263
|
addTaskContainerId.value = null
|
|
233
264
|
addTaskPrefill.value = null
|
|
234
265
|
}
|
|
266
|
+
function openReviewFriction(ctx: ReviewFrictionModalContext) {
|
|
267
|
+
reviewFrictionContext.value = ctx
|
|
268
|
+
}
|
|
269
|
+
function closeReviewFriction() {
|
|
270
|
+
reviewFrictionContext.value = null
|
|
271
|
+
}
|
|
235
272
|
function openAddRecurring(frameId: string) {
|
|
236
273
|
addRecurringFrameId.value = frameId
|
|
237
274
|
}
|
|
@@ -254,6 +291,7 @@ function createDocumentTaskModals(resetHubReturn: ResetHubReturn) {
|
|
|
254
291
|
taskImport,
|
|
255
292
|
addTaskContainerId,
|
|
256
293
|
addTaskPrefill,
|
|
294
|
+
reviewFrictionContext,
|
|
257
295
|
addRecurringFrameId,
|
|
258
296
|
createInitiativeFrameId,
|
|
259
297
|
openDocumentConnect,
|
|
@@ -270,6 +308,8 @@ function createDocumentTaskModals(resetHubReturn: ResetHubReturn) {
|
|
|
270
308
|
closeTaskImport,
|
|
271
309
|
openAddTask,
|
|
272
310
|
closeAddTask,
|
|
311
|
+
openReviewFriction,
|
|
312
|
+
closeReviewFriction,
|
|
273
313
|
openAddRecurring,
|
|
274
314
|
closeAddRecurring,
|
|
275
315
|
openCreateInitiative,
|
|
@@ -13,6 +13,10 @@ const DEFAULTS: WorkspaceSettings = {
|
|
|
13
13
|
artifactRetentionDays: 14,
|
|
14
14
|
kaizenEnabled: true,
|
|
15
15
|
delegateAgentsToRunnerPool: false,
|
|
16
|
+
reviewFrictionMode: 'off',
|
|
17
|
+
reviewFrictionWarnCount: 3,
|
|
18
|
+
reviewFrictionBlockCount: null,
|
|
19
|
+
reviewFrictionBlockStuckMinutes: null,
|
|
16
20
|
spendCurrency: null,
|
|
17
21
|
spendMonthlyLimit: null,
|
|
18
22
|
}
|
package/app/types/domain.ts
CHANGED
package/i18n/locales/de.json
CHANGED
|
@@ -738,6 +738,23 @@
|
|
|
738
738
|
"review": "Review",
|
|
739
739
|
"ralph": "Ralph-Schleife"
|
|
740
740
|
},
|
|
741
|
+
"reviewFriction": {
|
|
742
|
+
"heading": "Review-Rückstau-Bremse",
|
|
743
|
+
"body": "Erschwert das Anlegen neuer Aufgaben, solange fertige Arbeit auf die menschliche Prüfung wartet. Standardmäßig aus. „Warnen“ fügt eine Bestätigung hinzu; „Erzwingen“ verweigert das Anlegen ab einem harten Limit ganz.",
|
|
744
|
+
"mode": "Modus",
|
|
745
|
+
"modes": {
|
|
746
|
+
"off": "Aus",
|
|
747
|
+
"warn": "Warnen (Bestätigung nötig)",
|
|
748
|
+
"enforce": "Erzwingen (harte Sperre)"
|
|
749
|
+
},
|
|
750
|
+
"warnCount": "Warnen ab (Aufgaben in Prüfung)",
|
|
751
|
+
"enforceHint": "„Erzwingen“ verweigert das Anlegen von Aufgaben. Aktiviere unten mindestens einen harten Auslöser.",
|
|
752
|
+
"blockCountToggle": "Sperren, wenn zu viele Aufgaben in Prüfung sind",
|
|
753
|
+
"blockCount": "Sperren ab (Aufgaben in Prüfung)",
|
|
754
|
+
"blockStuckToggle": "Sperren, wenn eine Aufgabe zu lange wartet",
|
|
755
|
+
"blockStuckMinutes": "Sperren nach (Minuten Wartezeit)",
|
|
756
|
+
"needsTrigger": "Der Modus „Erzwingen“ benötigt mindestens einen aktiven harten Auslöser (eine Anzahl oder eine Wartezeit)."
|
|
757
|
+
},
|
|
741
758
|
"observability": {
|
|
742
759
|
"heading": "Agenten-Observability",
|
|
743
760
|
"body": "Speichere den vollständigen Kontext, der jedem Agenten bereitgestellt wird: die zusammengesetzten Prompts, die eingefügten Best-Practice-Fragmente und den vollständigen Inhalt der in seinen Container injizierten Dateien, sodass er später in der Observability-Ansicht inspiziert werden kann. Die Inhalte werden für denselben Zeitraum wie die LLM-Telemetrie pro Aufruf aufbewahrt. Deaktivieren, um die Speicherung zu stoppen (bestehende Snapshots werden durch die Aufbewahrung bereinigt).",
|
|
@@ -4060,6 +4077,22 @@
|
|
|
4060
4077
|
"mergeFailed": "Zusammenführen fehlgeschlagen",
|
|
4061
4078
|
"restartFailed": "Neustart fehlgeschlagen"
|
|
4062
4079
|
},
|
|
4080
|
+
"reviewFriction": {
|
|
4081
|
+
"warnTitle": "Aufgaben warten auf Prüfung",
|
|
4082
|
+
"blockedTitle": "Zu viele Aufgaben warten auf Prüfung",
|
|
4083
|
+
"warnToast": "Einige Aufgaben warten auf die menschliche Prüfung. Prüfe sie, bevor du mehr Arbeit anlegst.",
|
|
4084
|
+
"blockedToast": "Das Anlegen von Aufgaben ist gesperrt, bis der Prüf-Rückstau abgebaut ist.",
|
|
4085
|
+
"warnBody": "{count} Aufgabe(n) warten auf die menschliche Prüfung. Prüfe sie, bevor du mehr Arbeit anlegst.",
|
|
4086
|
+
"blockedCountBody": "Anlegen gesperrt: {count} Aufgabe(n) warten auf die menschliche Prüfung (Limit {threshold}). Baue zuerst den Prüf-Rückstau ab.",
|
|
4087
|
+
"blockedStuckBody": "Anlegen gesperrt: Eine Aufgabe wartet seit über {minutes} Minute(n) auf die menschliche Prüfung. Baue zuerst den Prüf-Rückstau ab.",
|
|
4088
|
+
"waitingHeading": "Warten auf Prüfung",
|
|
4089
|
+
"waiting": "{minutes} Min. Wartezeit",
|
|
4090
|
+
"untitled": "Aufgabe ohne Titel",
|
|
4091
|
+
"close": "Schließen",
|
|
4092
|
+
"createAnyway": "Trotzdem anlegen",
|
|
4093
|
+
"goReview": "Zur Prüfung",
|
|
4094
|
+
"badge": "{count} Aufgabe(n) warten auf Prüfung"
|
|
4095
|
+
},
|
|
4063
4096
|
"conflict": {
|
|
4064
4097
|
"title": {
|
|
4065
4098
|
"dependencies_unmet": "Durch Abhängigkeiten blockiert",
|