@cat-factory/app 0.195.2 → 0.196.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/README.md +10 -3
- package/app/components/brainstorm/BrainstormWindow.vue +11 -4
- package/app/components/clarity/ClarityReviewWindow.vue +11 -4
- package/app/components/initiative/InitiativePlanReview.vue +44 -37
- package/app/components/initiative/InitiativeTrackerWindow.vue +12 -9
- package/app/components/layout/SideBar.vue +13 -2
- package/app/components/layout/UiModeSwitcher.vue +66 -39
- package/app/components/panels/ResultWindowShell.logic.spec.ts +174 -0
- package/app/components/panels/ResultWindowShell.logic.ts +31 -0
- package/app/components/panels/ResultWindowShell.vue +37 -8
- package/app/components/panels/StepMetadataCard.vue +16 -0
- package/app/components/panels/StepRunMeta.vue +18 -0
- package/app/components/pipeline/PipelineBuilder.vue +46 -0
- package/app/components/prReview/PrReviewWindow.vue +15 -7
- package/app/components/requirements/RequirementsReviewWindow.vue +15 -5
- package/app/components/spec/ServiceSpecWindow.vue +7 -4
- package/app/components/testing/TestReportWindow.vue +11 -6
- package/app/composables/api/errors.ts +7 -0
- package/app/composables/usePipelineErrorToast.spec.ts +119 -5
- package/app/composables/usePipelineErrorToast.ts +140 -10
- package/app/composables/useStepPromptVariant.spec.ts +75 -0
- package/app/composables/useStepPromptVariant.ts +50 -0
- package/app/modular/nav-contributions.ts +3 -3
- package/app/stores/agents.spec.ts +30 -0
- package/app/stores/agents.ts +33 -1
- package/app/stores/pipelines/draftStepConfig.ts +24 -1
- package/app/stores/workspace/hydrate.ts +3 -0
- package/app/types/domain.ts +1 -0
- package/i18n/locales/de.json +25 -2
- package/i18n/locales/en.json +37 -2
- package/i18n/locales/es.json +25 -2
- package/i18n/locales/fr.json +25 -2
- package/i18n/locales/he.json +25 -2
- package/i18n/locales/it.json +25 -2
- package/i18n/locales/ja.json +25 -2
- package/i18n/locales/pl.json +25 -2
- package/i18n/locales/tr.json +25 -2
- package/i18n/locales/uk.json +25 -2
- package/package.json +2 -2
|
@@ -11,10 +11,21 @@
|
|
|
11
11
|
* locale missing the key) and stays untranslated — the contract is "if a server message must be
|
|
12
12
|
* localizable, the backend emits a code and the frontend maps it", not "translate arbitrary server
|
|
13
13
|
* prose on the client".
|
|
14
|
+
*
|
|
15
|
+
* G2 closes the same gap for everything that is NOT a 409: this composable is the funnel every
|
|
16
|
+
* other failure drains into, and it used to show the backend's prose verbatim as the description —
|
|
17
|
+
* so a non-English user read English, and an internal 500's fixed `Internal server error` was the
|
|
18
|
+
* whole of what they were told. Those now resolve translated copy from the envelope's STATUS CLASS
|
|
19
|
+
* (`error.code`, the `ApiErrorCode` union) and keep the untranslated detail — the prose, a
|
|
20
|
+
* validation 400's `issues`, and the `requestId` an operator can grep — one click away behind
|
|
21
|
+
* "Show details". Two rules follow from that split: the description says what a user can act on,
|
|
22
|
+
* the disclosure carries what a user quotes to someone else; and a raw string is never the FIRST
|
|
23
|
+
* thing shown, however good it is (many of them are — the elaborate remedies this initiative
|
|
24
|
+
* added — which is exactly why the detail stays reachable rather than being dropped).
|
|
14
25
|
*/
|
|
15
26
|
|
|
16
|
-
import type { ConflictReason } from '@cat-factory/contracts'
|
|
17
|
-
import { apiErrorEnvelope } from './api/errors'
|
|
27
|
+
import type { ApiErrorCode, ConflictReason } from '@cat-factory/contracts'
|
|
28
|
+
import { apiErrorEnvelope, apiErrorStatus } from './api/errors'
|
|
18
29
|
|
|
19
30
|
/** The parsed shape of a backend conflict (`{ error: { code: 'conflict', details } }`). */
|
|
20
31
|
interface ConflictDetails {
|
|
@@ -235,6 +246,87 @@ export function parseConflict(
|
|
|
235
246
|
/** The non-null parsed shape of a backend conflict, as returned by {@link parseConflict}. */
|
|
236
247
|
type ParsedConflict = NonNullable<ReturnType<typeof parseConflict>>
|
|
237
248
|
|
|
249
|
+
/**
|
|
250
|
+
* Generic translated description per STATUS CLASS, for a failure no `reason` code narrows.
|
|
251
|
+
*
|
|
252
|
+
* Exhaustive over the wire union (minus `conflict`, which structurally cannot arrive here —
|
|
253
|
+
* {@link parseConflict} intercepts every envelope carrying that code, so a mapping for it would be
|
|
254
|
+
* dead copy in ten locales), which makes the `Record` the drift guard: a new `ApiErrorCode` fails
|
|
255
|
+
* this typecheck until it has wording. The copy is deliberately about the STATUS CLASS and nothing
|
|
256
|
+
* else — it is what we can say truthfully without having read the specific failure, so it names
|
|
257
|
+
* the shape of the remedy ("sign in again", "your deployment hasn't wired this", "wait and retry")
|
|
258
|
+
* and leaves the specifics to the detail disclosure.
|
|
259
|
+
*/
|
|
260
|
+
const GENERIC_DESCRIPTION_KEYS: Record<Exclude<ApiErrorCode, 'conflict'>, string> = {
|
|
261
|
+
not_found: 'errors.generic.description.not_found',
|
|
262
|
+
validation: 'errors.generic.description.validation',
|
|
263
|
+
credential_required: 'errors.generic.description.credential_required',
|
|
264
|
+
forbidden: 'errors.generic.description.forbidden',
|
|
265
|
+
unavailable: 'errors.generic.description.unavailable',
|
|
266
|
+
unauthorized: 'errors.generic.description.unauthorized',
|
|
267
|
+
rate_limited: 'errors.generic.description.rate_limited',
|
|
268
|
+
internal: 'errors.generic.description.internal',
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* The request never reached a server that answered in our envelope shape — offline, DNS, a dropped
|
|
273
|
+
* connection, CORS. Distinct from {@link UNEXPECTED_DESCRIPTION_KEY} on purpose: this one's remedy
|
|
274
|
+
* is on the USER's side (check the connection), which is the opposite of "the server is broken".
|
|
275
|
+
*/
|
|
276
|
+
const NETWORK_DESCRIPTION_KEY = 'errors.generic.description.network'
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Something answered with an HTTP status but not one of our envelopes (an edge/proxy 502 page, a
|
|
280
|
+
* gateway timeout), or answered with a `code` this build doesn't know. Reported as an unexpected
|
|
281
|
+
* SERVER-side failure rather than folded into the network case.
|
|
282
|
+
*/
|
|
283
|
+
const UNEXPECTED_DESCRIPTION_KEY = 'errors.generic.description.unexpected'
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* A non-conflict failure, split into the part that gets TRANSLATED and the parts that stay raw.
|
|
287
|
+
* Pure (no i18n, no store) so the classification is unit-testable on its own; the composable
|
|
288
|
+
* turns it into a toast.
|
|
289
|
+
*/
|
|
290
|
+
export interface GenericFailure {
|
|
291
|
+
/** i18n key for the translated description shown up front. */
|
|
292
|
+
descriptionKey: string
|
|
293
|
+
/** The backend's untranslated prose, when it sent any (absent for a bare network fault). */
|
|
294
|
+
message: string | null
|
|
295
|
+
/** `path: message` entries from a request-validation 400, in wire order. */
|
|
296
|
+
issues: string[]
|
|
297
|
+
/** The envelope's correlation id, so the user can quote it at whoever reads the logs. */
|
|
298
|
+
requestId: string | null
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Classify a NON-conflict failure for presentation. Never throws and never returns an empty
|
|
303
|
+
* `descriptionKey`: an error this function cannot recognise at all still gets the network or
|
|
304
|
+
* unexpected-failure wording, because a toast with no description reads as a successful action.
|
|
305
|
+
*/
|
|
306
|
+
export function describeGenericFailure(error: unknown): GenericFailure {
|
|
307
|
+
const envelope = apiErrorEnvelope(error)
|
|
308
|
+
// Read through a widened alias rather than casting the wire string to the union: a `code` we
|
|
309
|
+
// don't know must resolve to `undefined`, which is exactly what the alias's index signature
|
|
310
|
+
// says and what a cast would have hidden. The narrow Record above stays the drift guard.
|
|
311
|
+
const byCode: Readonly<Record<string, string | undefined>> = GENERIC_DESCRIPTION_KEYS
|
|
312
|
+
const mapped = envelope?.code ? byCode[envelope.code] : undefined
|
|
313
|
+
// No envelope at all AND no status ⇒ nothing answered; with a status, something did.
|
|
314
|
+
const unrecognised =
|
|
315
|
+
!envelope && apiErrorStatus(error) === undefined
|
|
316
|
+
? NETWORK_DESCRIPTION_KEY
|
|
317
|
+
: UNEXPECTED_DESCRIPTION_KEY
|
|
318
|
+
return {
|
|
319
|
+
descriptionKey: mapped ?? unrecognised,
|
|
320
|
+
// `ApiError.message` is the envelope's prose, or a synthesised `Request failed (HTTP n)`; for
|
|
321
|
+
// a non-API throw it is the JS error text. Either way it is detail, never the headline.
|
|
322
|
+
message: error instanceof Error ? error.message : error == null ? null : String(error),
|
|
323
|
+
issues: (envelope?.issues ?? []).map((issue) =>
|
|
324
|
+
issue.path ? `${issue.path}: ${issue.message}` : issue.message,
|
|
325
|
+
),
|
|
326
|
+
requestId: typeof envelope?.requestId === 'string' ? envelope.requestId : null,
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
238
330
|
export function usePipelineErrorToast() {
|
|
239
331
|
const toast = useToast()
|
|
240
332
|
const ui = useUiStore()
|
|
@@ -448,6 +540,51 @@ export function usePipelineErrorToast() {
|
|
|
448
540
|
})
|
|
449
541
|
}
|
|
450
542
|
|
|
543
|
+
/**
|
|
544
|
+
* Everything that is NOT a 409: a translated status-class description, with the raw detail
|
|
545
|
+
* behind a "Show details" button that swaps it into the same toast (G2).
|
|
546
|
+
*
|
|
547
|
+
* The reveal is an UPDATE rather than a second toast so the two readings can't sit on screen
|
|
548
|
+
* disagreeing, and it makes the toast sticky at the same time — the detail is what someone
|
|
549
|
+
* copies into a bug report, and a ~5s auto-dismiss takes it away mid-copy. `actions: []` has to
|
|
550
|
+
* be passed explicitly: `update` merges over the existing toast, so an omitted `actions` would
|
|
551
|
+
* leave a "Show details" button that is now a no-op.
|
|
552
|
+
*
|
|
553
|
+
* No detail worth showing (a network fault with an unhelpful `message` and no correlation id)
|
|
554
|
+
* ⇒ no button at all, rather than a disclosure that reveals nothing.
|
|
555
|
+
*/
|
|
556
|
+
function presentGenericFailure(error: unknown, fallbackTitleKey: string): void {
|
|
557
|
+
const failure = describeGenericFailure(error)
|
|
558
|
+
const detail = [
|
|
559
|
+
failure.message,
|
|
560
|
+
failure.issues.join(', '),
|
|
561
|
+
failure.requestId ? t('errors.generic.requestId', { id: failure.requestId }) : '',
|
|
562
|
+
]
|
|
563
|
+
.filter((part) => part && part.trim().length > 0)
|
|
564
|
+
.join(' · ')
|
|
565
|
+
// No `te` guard: the key comes from a Record exhaustive over the wire union and every entry
|
|
566
|
+
// ships in the base `en` catalog, so a locale missing it renders English via `fallbackLocale`
|
|
567
|
+
// (better than the raw prose this replaced) and a bare key can never leak.
|
|
568
|
+
const added = toast.add({
|
|
569
|
+
title: t(fallbackTitleKey),
|
|
570
|
+
description: t(failure.descriptionKey),
|
|
571
|
+
color: 'error',
|
|
572
|
+
icon: 'i-lucide-triangle-alert',
|
|
573
|
+
...(detail
|
|
574
|
+
? {
|
|
575
|
+
actions: [
|
|
576
|
+
{
|
|
577
|
+
label: t('errors.generic.showDetail'),
|
|
578
|
+
icon: 'i-lucide-info',
|
|
579
|
+
onClick: () =>
|
|
580
|
+
toast.update(added.id, { description: detail, duration: 0, actions: [] }),
|
|
581
|
+
},
|
|
582
|
+
],
|
|
583
|
+
}
|
|
584
|
+
: {}),
|
|
585
|
+
})
|
|
586
|
+
}
|
|
587
|
+
|
|
451
588
|
/**
|
|
452
589
|
* Present `error` as a toast. `fallbackTitleKey` is an i18n message key used for
|
|
453
590
|
* non-conflict failures and any conflict reason without a dedicated title.
|
|
@@ -459,14 +596,7 @@ export function usePipelineErrorToast() {
|
|
|
459
596
|
presentMappedConflict(conflict, fallbackTitleKey)
|
|
460
597
|
return
|
|
461
598
|
}
|
|
462
|
-
|
|
463
|
-
// Not a conflict (a 4xx/5xx or a network fault) — surface its message plainly.
|
|
464
|
-
toast.add({
|
|
465
|
-
title: t(fallbackTitleKey),
|
|
466
|
-
description: error instanceof Error ? error.message : String(error),
|
|
467
|
-
color: 'error',
|
|
468
|
-
icon: 'i-lucide-triangle-alert',
|
|
469
|
-
})
|
|
599
|
+
presentGenericFailure(error, fallbackTitleKey)
|
|
470
600
|
}
|
|
471
601
|
|
|
472
602
|
return { present }
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { describe, expect, it, vi, beforeEach } from 'vitest'
|
|
2
|
+
import type { PipelineStep } from '~/types/execution'
|
|
3
|
+
import { useStepPromptVariant } from '~/composables/useStepPromptVariant'
|
|
4
|
+
import en from '../../i18n/locales/en.json'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* What the run panels report about a step's agent-kind VARIANT.
|
|
8
|
+
*
|
|
9
|
+
* The property under test is that they report what the DISPATCH did, not what the pipeline asked
|
|
10
|
+
* for. A step can name a variant whose text never reached its prompt — the workspace's own edit of
|
|
11
|
+
* that kind displaces a variant's replacement, and a variant can be withdrawn mid-run — and a
|
|
12
|
+
* panel that echoed the selection would confirm a variation that did not run. Each losing
|
|
13
|
+
* disposition therefore gets its own note.
|
|
14
|
+
*
|
|
15
|
+
* Assertions are on KEYS, never English text, so they stay locale-agnostic (the `t` spy echoes
|
|
16
|
+
* its key) — but every key is checked against the real `en.json` so a typo can't pass.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
function hasKey(path: string): boolean {
|
|
20
|
+
return (
|
|
21
|
+
path.split('.').reduce<unknown>((node, seg) => {
|
|
22
|
+
return node && typeof node === 'object' ? (node as Record<string, unknown>)[seg] : undefined
|
|
23
|
+
}, en) !== undefined
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
vi.stubGlobal('useI18n', () => ({ t: (key: string) => key, te: hasKey }))
|
|
29
|
+
vi.stubGlobal('useAgentsStore', () => ({
|
|
30
|
+
variantLabel: (id: string) => (id === 'org:tdd' ? 'TDD-first' : id),
|
|
31
|
+
}))
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
function step(promptVariant?: PipelineStep['promptVariant']): PipelineStep {
|
|
35
|
+
return {
|
|
36
|
+
agentKind: 'coder',
|
|
37
|
+
state: 'done',
|
|
38
|
+
...(promptVariant ? { promptVariant } : {}),
|
|
39
|
+
} as PipelineStep
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe('useStepPromptVariant', () => {
|
|
43
|
+
it('reports nothing for a step that ran the shipped prompt', () => {
|
|
44
|
+
expect(useStepPromptVariant(() => step()).value).toBeNull()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('reports the label with NO note when the variant fully applied', () => {
|
|
48
|
+
const variant = useStepPromptVariant(() => step({ id: 'org:tdd', applied: 'full' })).value
|
|
49
|
+
expect(variant).toEqual({ label: 'TDD-first', note: null })
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('reports a note when the workspace prompt displaced the variant entirely', () => {
|
|
53
|
+
// The case that used to read as a plain confirmation the variant ran.
|
|
54
|
+
const variant = useStepPromptVariant(() => step({ id: 'org:tdd', applied: 'superseded' })).value
|
|
55
|
+
expect(variant?.label).toBe('TDD-first')
|
|
56
|
+
expect(variant?.note).toBe('panels.stepMeta.promptVariantSuperseded')
|
|
57
|
+
expect(hasKey(variant!.note!)).toBe(true)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('distinguishes a partly-applied variant from a fully displaced one', () => {
|
|
61
|
+
const note = useStepPromptVariant(() => step({ id: 'org:tdd', applied: 'addition-only' })).value
|
|
62
|
+
?.note
|
|
63
|
+
expect(note).toBe('panels.stepMeta.promptVariantAdditionOnly')
|
|
64
|
+
expect(hasKey(note!)).toBe(true)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('distinguishes a WITHDRAWN variant, and still names the id it asked for', () => {
|
|
68
|
+
// The label falls back to the raw id: the step really was configured to run it, so rendering
|
|
69
|
+
// nothing would show a varied step as if it were the stock kind.
|
|
70
|
+
const variant = useStepPromptVariant(() => step({ id: 'org:gone', applied: 'withdrawn' })).value
|
|
71
|
+
expect(variant?.label).toBe('org:gone')
|
|
72
|
+
expect(variant?.note).toBe('panels.stepMeta.promptVariantWithdrawn')
|
|
73
|
+
expect(hasKey(variant!.note!)).toBe(true)
|
|
74
|
+
})
|
|
75
|
+
})
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { computed, type ComputedRef } from 'vue'
|
|
2
|
+
import type { PipelineStep } from '~/types/execution'
|
|
3
|
+
|
|
4
|
+
// The deployment-registered agent-kind VARIANT a step ran under, as the run panels report it.
|
|
5
|
+
//
|
|
6
|
+
// Read off the dispatch-time PIN (`step.promptVariant`), never off `stepOptions.agentVariantId`:
|
|
7
|
+
// the option is what the pipeline ASKED for, and the two diverge whenever the workspace has also
|
|
8
|
+
// edited that kind's prompt — the workspace is the narrower tier, so it displaces a variant's own
|
|
9
|
+
// replacement. A panel keyed on the selection would report `Prompt variant: TDD-first` on a step
|
|
10
|
+
// whose prompt contains none of that variant's text, which is worse than saying nothing: it reads
|
|
11
|
+
// as confirmation. So each losing disposition gets its own note rather than being flattened into
|
|
12
|
+
// the label, because they need different fixes (drop the workspace's edit / use an addition
|
|
13
|
+
// instead of a replacement / re-register the variant).
|
|
14
|
+
//
|
|
15
|
+
// Absent before the step dispatches, exactly like `step.model` beside it: what a step RAN under is
|
|
16
|
+
// not a fact until it runs.
|
|
17
|
+
|
|
18
|
+
/** The dispatch-time pin a panel reads (`PipelineStep.promptVariant`), narrowed to non-null. */
|
|
19
|
+
type PromptVariantPin = NonNullable<PipelineStep['promptVariant']>
|
|
20
|
+
|
|
21
|
+
/** What a panel shows for a step's variant: the label, plus a note when it did not fully apply. */
|
|
22
|
+
export interface StepPromptVariant {
|
|
23
|
+
/** The variant's registered label, falling back to its raw id when it is no longer registered. */
|
|
24
|
+
label: string
|
|
25
|
+
/** Why the variant's text did not (fully) reach this step's prompt; null when it did. */
|
|
26
|
+
note: string | null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function useStepPromptVariant(
|
|
30
|
+
step: () => PipelineStep,
|
|
31
|
+
): ComputedRef<StepPromptVariant | null> {
|
|
32
|
+
const agents = useAgentsStore()
|
|
33
|
+
const { t } = useI18n()
|
|
34
|
+
// One STATIC literal `t()` per member of the closed disposition union, not a key assembled from
|
|
35
|
+
// `applied`: the typed-message-key check and the catalog drift guard both read literal keys, and
|
|
36
|
+
// a runtime-assembled one is invisible to them. The `Record` type is the exhaustiveness half —
|
|
37
|
+
// a new disposition fails to compile until it has copy.
|
|
38
|
+
const NOTE: Record<PromptVariantPin['applied'], () => string | null> = {
|
|
39
|
+
full: () => null,
|
|
40
|
+
'addition-only': () => t('panels.stepMeta.promptVariantAdditionOnly'),
|
|
41
|
+
superseded: () => t('panels.stepMeta.promptVariantSuperseded'),
|
|
42
|
+
withdrawn: () => t('panels.stepMeta.promptVariantWithdrawn'),
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return computed(() => {
|
|
46
|
+
const pin = step().promptVariant
|
|
47
|
+
if (!pin) return null
|
|
48
|
+
return { label: agents.variantLabel(pin.id), note: NOTE[pin.applied]() }
|
|
49
|
+
})
|
|
50
|
+
}
|
|
@@ -460,9 +460,9 @@ export const NAV_CONTRIBUTIONS: readonly NavContribution[] = [
|
|
|
460
460
|
{
|
|
461
461
|
// Deliberately NOT `advanced`: this is the way BACK. Basic mode is the shipped default, so
|
|
462
462
|
// for most users the sidebar switcher is their first sight of the tier — and in the basic
|
|
463
|
-
// rail it
|
|
464
|
-
// product on. The palette is reachable in both tiers, searchable by name, and
|
|
465
|
-
// "the primary way to reach every action", so the tier belongs in it.
|
|
463
|
+
// rail it collapses to a single toggle, which is a thin thread to hang the entire advanced
|
|
464
|
+
// half of the product on. The palette is reachable in both tiers, searchable by name, and
|
|
465
|
+
// is already "the primary way to reach every action", so the tier belongs in it.
|
|
466
466
|
id: 'ui-mode',
|
|
467
467
|
labelKey: 'layout.commandBar.cmd.toggleUiMode',
|
|
468
468
|
icon: 'i-lucide-toggle-right',
|
|
@@ -101,4 +101,34 @@ describe('agents store — custom-kind catalog (slice 2)', () => {
|
|
|
101
101
|
expect(created.category).toBeUndefined() // lands in the palette "custom" bucket
|
|
102
102
|
expect(agentKindMeta(created.kind).label).toBe('My Agent')
|
|
103
103
|
})
|
|
104
|
+
|
|
105
|
+
it('holds agent-kind VARIANTS apart from the palette catalog', () => {
|
|
106
|
+
// A variant is a per-step OPTION on a kind that is already in the palette, not a kind of its
|
|
107
|
+
// own — so it must never reach `archetypes` / `agentKindMeta`, or it would become placeable.
|
|
108
|
+
const agents = useAgentsStore()
|
|
109
|
+
agents.hydrateVariants([
|
|
110
|
+
{ id: 'org:tdd', baseKind: 'coder', label: 'TDD-first' },
|
|
111
|
+
{ id: 'org:sec', baseKind: 'pr-reviewer', label: 'Security lens' },
|
|
112
|
+
])
|
|
113
|
+
expect(agents.variantsForKind('coder').map((v) => v.id)).toEqual(['org:tdd'])
|
|
114
|
+
expect(agents.variantsForKind('architect')).toEqual([])
|
|
115
|
+
expect(agents.archetypes.some((a) => a.kind === 'org:tdd')).toBe(false)
|
|
116
|
+
expect(isKnownAgentKind('org:tdd')).toBe(false)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('falls back to a variant id the deployment no longer registers', () => {
|
|
120
|
+
// A step really is configured to run that variant; rendering nothing would show a varied step
|
|
121
|
+
// as if it were the stock kind.
|
|
122
|
+
const agents = useAgentsStore()
|
|
123
|
+
agents.hydrateVariants([{ id: 'org:tdd', baseKind: 'coder', label: 'TDD-first' }])
|
|
124
|
+
expect(agents.variantLabel('org:tdd')).toBe('TDD-first')
|
|
125
|
+
expect(agents.variantLabel('org:withdrawn')).toBe('org:withdrawn')
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('swaps the variant list wholesale on re-hydrate (per-workspace snapshot)', () => {
|
|
129
|
+
const agents = useAgentsStore()
|
|
130
|
+
agents.hydrateVariants([{ id: 'org:tdd', baseKind: 'coder', label: 'TDD-first' }])
|
|
131
|
+
agents.hydrateVariants([])
|
|
132
|
+
expect(agents.variantsForKind('coder')).toEqual([])
|
|
133
|
+
})
|
|
104
134
|
})
|
package/app/stores/agents.ts
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
SYSTEM_AGENT_META,
|
|
11
11
|
uid,
|
|
12
12
|
} from '~/utils/catalog'
|
|
13
|
-
import type { AgentArchetype, AgentKind, CustomAgentKind } from '~/types/domain'
|
|
13
|
+
import type { AgentArchetype, AgentKind, AgentKindVariant, CustomAgentKind } from '~/types/domain'
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* The agent palette catalog (slice 2 of the modular-vue adoption —
|
|
@@ -39,6 +39,12 @@ export const useAgentsStore = defineStore('agents', () => {
|
|
|
39
39
|
const capabilitiesManifest = ref<RemoteModuleManifest<AppSlots> | null>(null)
|
|
40
40
|
// In-UI, client-only prototype agents created via the "add agent" modal.
|
|
41
41
|
const runtimeAgents = ref<AgentArchetype[]>([])
|
|
42
|
+
// The deployment's registered agent-kind VARIANTS (alternate prompts for EXISTING kinds), from
|
|
43
|
+
// the snapshot. Deliberately NOT part of the capability manifest above: a variant is not a
|
|
44
|
+
// palette block and has no result view — it is a per-step OPTION on a kind that is already
|
|
45
|
+
// there — so folding it into the kind catalog would make it placeable, which is exactly what
|
|
46
|
+
// the backend model says it is not. A straight replace, like the skills catalog it mirrors.
|
|
47
|
+
const variants = ref<AgentKindVariant[]>([])
|
|
42
48
|
|
|
43
49
|
/**
|
|
44
50
|
* The merged CUSTOM catalog (consumer-slot → backend-manifest → runtime), each
|
|
@@ -134,6 +140,28 @@ export const useAgentsStore = defineStore('agents', () => {
|
|
|
134
140
|
capabilitiesManifest.value = manifest
|
|
135
141
|
}
|
|
136
142
|
|
|
143
|
+
/** Hydrate the deployment's registered agent-kind variants from the snapshot (straight replace). */
|
|
144
|
+
function hydrateVariants(list: readonly AgentKindVariant[]) {
|
|
145
|
+
variants.value = [...list]
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The variants registered for one kind — what the pipeline builder offers as that step's
|
|
150
|
+
* alternate prompt. Empty for every kind on the stock product.
|
|
151
|
+
*/
|
|
152
|
+
function variantsForKind(kind: AgentKind): AgentKindVariant[] {
|
|
153
|
+
return variants.value.filter((variant) => variant.baseKind === kind)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* A variant's display label, or the raw id when the deployment no longer registers it. The id
|
|
158
|
+
* is the honest fallback: a step really is configured to run that variant, and rendering
|
|
159
|
+
* nothing would show a varied step as if it were the stock kind.
|
|
160
|
+
*/
|
|
161
|
+
function variantLabel(id: string): string {
|
|
162
|
+
return variants.value.find((variant) => variant.id === id)?.label ?? id
|
|
163
|
+
}
|
|
164
|
+
|
|
137
165
|
return {
|
|
138
166
|
archetypes,
|
|
139
167
|
customArchetypes,
|
|
@@ -141,5 +169,9 @@ export const useAgentsStore = defineStore('agents', () => {
|
|
|
141
169
|
addAgent,
|
|
142
170
|
registerConsumerKinds,
|
|
143
171
|
hydrateCapabilities,
|
|
172
|
+
variants,
|
|
173
|
+
hydrateVariants,
|
|
174
|
+
variantsForKind,
|
|
175
|
+
variantLabel,
|
|
144
176
|
}
|
|
145
177
|
})
|
|
@@ -6,7 +6,7 @@ import { defaultConsensusConfig, type PipelinesContext } from './context'
|
|
|
6
6
|
* The pipeline-builder draft's PER-STEP CONFIG toggles: consensus (inline panel and the workspace
|
|
7
7
|
* consensus-GROUP tier set), the human approval gate, the estimate gate on a companion step, the
|
|
8
8
|
* follow-up and test-QC companions, the per-step enable flag, and the `StepOptions` bag
|
|
9
|
-
* (requirements auto-recommendation, the picked skill).
|
|
9
|
+
* (requirements auto-recommendation, the picked skill, the picked agent-kind variant).
|
|
10
10
|
*
|
|
11
11
|
* Split out of `./draftActions`, which owns the draft's STRUCTURE (insert / remove / reorder /
|
|
12
12
|
* units). Every function here reads and writes one of the parallel per-step arrays at an index and
|
|
@@ -139,6 +139,27 @@ export function createPipelineStepConfigActions(ctx: PipelinesContext) {
|
|
|
139
139
|
draftStepOptions.value[index] = Object.keys(next).length ? next : null
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
/**
|
|
143
|
+
* The agent-kind VARIANT picked for the draft step at `index` (its
|
|
144
|
+
* `stepOptions.agentVariantId`), or undefined when it runs the kind's shipped prompt.
|
|
145
|
+
*/
|
|
146
|
+
function draftAgentVariantId(index: number): string | undefined {
|
|
147
|
+
return draftStepOptions.value[index]?.agentVariantId
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Set (or clear) the picked variant on the draft step at `index`. Merges into the step's
|
|
152
|
+
* `StepOptions` bag rather than clobbering it; clearing drops the field and, if the bag
|
|
153
|
+
* empties, the whole entry — exactly like the other options here, so a step back on the
|
|
154
|
+
* shipped prompt persists nothing.
|
|
155
|
+
*/
|
|
156
|
+
function setDraftAgentVariantId(index: number, agentVariantId: string | undefined) {
|
|
157
|
+
const next: StepOptions = { ...draftStepOptions.value[index] }
|
|
158
|
+
if (agentVariantId) next.agentVariantId = agentVariantId
|
|
159
|
+
else delete next.agentVariantId
|
|
160
|
+
draftStepOptions.value[index] = Object.keys(next).length ? next : null
|
|
161
|
+
}
|
|
162
|
+
|
|
142
163
|
/**
|
|
143
164
|
* The output-token ceiling pinned on the draft step at `index`, or undefined when the step
|
|
144
165
|
* inherits (the workspace's per-kind setting, else the deployment default).
|
|
@@ -174,6 +195,8 @@ export function createPipelineStepConfigActions(ctx: PipelinesContext) {
|
|
|
174
195
|
toggleDraftAutoRecommend,
|
|
175
196
|
draftSkillId,
|
|
176
197
|
setDraftSkillId,
|
|
198
|
+
draftAgentVariantId,
|
|
199
|
+
setDraftAgentVariantId,
|
|
177
200
|
draftMaxOutputTokens,
|
|
178
201
|
setDraftMaxOutputTokens,
|
|
179
202
|
}
|
|
@@ -97,6 +97,9 @@ export function applySnapshotToStores(snapshot: WorkspaceSnapshot, boardSince?:
|
|
|
97
97
|
snapshot.customTaskTypes ?? [],
|
|
98
98
|
)
|
|
99
99
|
useAgentsStore().hydrateCapabilities(capabilities)
|
|
100
|
+
// The deployment's registered agent-kind variants (alternate prompts for existing kinds), so
|
|
101
|
+
// the builder can offer them per step and the run views can name the one a step ran under.
|
|
102
|
+
useAgentsStore().hydrateVariants(snapshot.agentKindVariants ?? [])
|
|
100
103
|
useTaskTypesStore().hydrateCapabilities(capabilities)
|
|
101
104
|
// The account's repo-sourced Claude Skills catalog (shared across its workspaces), so the
|
|
102
105
|
// pipeline builder's per-step skill picker has its options. A straight replace.
|
package/app/types/domain.ts
CHANGED
package/i18n/locales/de.json
CHANGED
|
@@ -1647,7 +1647,11 @@
|
|
|
1647
1647
|
"approved": "Freigegeben",
|
|
1648
1648
|
"changes_requested": "Änderungen angefordert",
|
|
1649
1649
|
"rejected": "Abgelehnt"
|
|
1650
|
-
}
|
|
1650
|
+
},
|
|
1651
|
+
"promptVariant": "Prompt-Variante",
|
|
1652
|
+
"promptVariantAdditionOnly": "Nur die Ergänzung dieser Variante wurde angewendet. Der Workspace-Prompt für diesen Agenten hat ihren Basistext ersetzt.",
|
|
1653
|
+
"promptVariantSuperseded": "Diese Variante wurde nicht angewendet. Der Workspace-Prompt für diesen Agenten hatte Vorrang.",
|
|
1654
|
+
"promptVariantWithdrawn": "Diese Variante ist nicht mehr registriert, daher lief der Schritt mit dem ausgelieferten Prompt."
|
|
1651
1655
|
},
|
|
1652
1656
|
"stepDetail": {
|
|
1653
1657
|
"contents": "Inhalte",
|
|
@@ -3691,7 +3695,9 @@
|
|
|
3691
3695
|
"consensusGroups": "Konsensgruppen",
|
|
3692
3696
|
"consensusGroupsHint": "Wähle die wiederverwendbaren Panels, auf die dieser Schritt hochstufen darf. Jedes hat seine eigene Schätzungsschwelle; es läuft das anspruchsvollste, das die Aufgabe erreicht.",
|
|
3693
3697
|
"consensusGroupsActive": "Dieser Schritt führt die ausgewählte Gruppe aus. Hebe die Auswahl aller Gruppen auf, um die Teilnehmer hier zu konfigurieren.",
|
|
3694
|
-
"consensusGroupAlways": "immer"
|
|
3698
|
+
"consensusGroupAlways": "immer",
|
|
3699
|
+
"variantLabel": "Prompt-Variante",
|
|
3700
|
+
"variantShipped": "Ausgelieferter Prompt"
|
|
3695
3701
|
},
|
|
3696
3702
|
"progress": {
|
|
3697
3703
|
"status": {
|
|
@@ -4536,6 +4542,7 @@
|
|
|
4536
4542
|
"advanced": "Erweitert",
|
|
4537
4543
|
"basicHint": "Nur die alltäglichen Werkzeuge. Für alles auf „Erweitert“ umschalten.",
|
|
4538
4544
|
"advancedHint": "Alle Bereiche und alle Ausführungsoptionen.",
|
|
4545
|
+
"switchTo": "Zu „{mode}“ wechseln",
|
|
4539
4546
|
"pinned": "Von dieser Installation festgelegt"
|
|
4540
4547
|
},
|
|
4541
4548
|
"common": {
|
|
@@ -4634,6 +4641,22 @@
|
|
|
4634
4641
|
"reports": "Berichte"
|
|
4635
4642
|
},
|
|
4636
4643
|
"errors": {
|
|
4644
|
+
"generic": {
|
|
4645
|
+
"showDetail": "Details anzeigen",
|
|
4646
|
+
"requestId": "Anfrage-ID {id}",
|
|
4647
|
+
"description": {
|
|
4648
|
+
"not_found": "Das Objekt dieser Aktion existiert nicht mehr. Lade die Seite neu, um den aktuellen Stand zu sehen.",
|
|
4649
|
+
"validation": "Der Server hat diese Anfrage als ungültig abgelehnt. Prüfe die eingegebenen Werte und versuche es erneut.",
|
|
4650
|
+
"credential_required": "Diese Aktion benötigt einen persönlichen Zugang, der nicht entsperrt ist. Entsperre dein Abonnement oder verbinde es neu und versuche es dann erneut.",
|
|
4651
|
+
"forbidden": "Deine Rolle in diesem Workspace erlaubt diese Aktion nicht. Bitte einen Workspace-Admin, sie auszuführen oder deine Rolle zu erweitern.",
|
|
4652
|
+
"unavailable": "In diesem Deployment ist die für diese Aktion benötigte Funktion nicht konfiguriert. Bitte den Betreiber des Deployments, sie einzurichten.",
|
|
4653
|
+
"unauthorized": "Deine Sitzung ist nicht mehr gültig. Melde dich erneut an und versuche es dann noch einmal.",
|
|
4654
|
+
"rate_limited": "Zu viele Anfragen in kurzer Zeit. Warte einen Moment und versuche es erneut.",
|
|
4655
|
+
"internal": "Auf dem Server ist ein Fehler aufgetreten. Versuche es erneut und gib die Details an den Betreiber des Deployments weiter, wenn es weiterhin auftritt.",
|
|
4656
|
+
"network": "Der Server war nicht erreichbar. Prüfe deine Verbindung und versuche es erneut.",
|
|
4657
|
+
"unexpected": "Der Server hat eine unerwartete Antwort zurückgegeben. Versuche es erneut und gib die Details an den Betreiber des Deployments weiter, wenn es weiterhin auftritt."
|
|
4658
|
+
}
|
|
4659
|
+
},
|
|
4637
4660
|
"action": {
|
|
4638
4661
|
"retryFailed": "Wiederholung fehlgeschlagen",
|
|
4639
4662
|
"startFailed": "Start fehlgeschlagen",
|
package/i18n/locales/en.json
CHANGED
|
@@ -32,6 +32,10 @@
|
|
|
32
32
|
"advanced": "Advanced",
|
|
33
33
|
"basicHint": "Everyday tools only. Switch to Advanced to see everything.",
|
|
34
34
|
"advancedHint": "Every destination and every run option.",
|
|
35
|
+
"switchTo": "Switch to {mode}",
|
|
36
|
+
"@switchTo": {
|
|
37
|
+
"description": "Tooltip on the collapsed-sidebar tier button. {mode} is the OTHER interface tier name, i.e. the Basic or Advanced value from this same section - inflect the surrounding words to agree with it."
|
|
38
|
+
},
|
|
35
39
|
"pinned": "Set by this deployment"
|
|
36
40
|
},
|
|
37
41
|
"common": {
|
|
@@ -515,6 +519,31 @@
|
|
|
515
519
|
"importsOnAdd": "imports on add"
|
|
516
520
|
},
|
|
517
521
|
"errors": {
|
|
522
|
+
"generic": {
|
|
523
|
+
"showDetail": "Show details",
|
|
524
|
+
"@showDetail": {
|
|
525
|
+
"description": "Button label on an error toast that swaps the generic explanation for the raw technical detail (the server message, validation issues, request id). An imperative verb phrase, not a heading."
|
|
526
|
+
},
|
|
527
|
+
"requestId": "Request ID {id}",
|
|
528
|
+
"@requestId": {
|
|
529
|
+
"description": "Keep the {id} placeholder. \"Request ID\" is the correlation id an operator greps for in the server logs, so keep it recognizable as a technical identifier rather than translating it as prose."
|
|
530
|
+
},
|
|
531
|
+
"description": {
|
|
532
|
+
"not_found": "What this action refers to no longer exists. Reload the page to see the current state.",
|
|
533
|
+
"validation": "The server rejected this request as invalid. Check the values you entered, then try again.",
|
|
534
|
+
"credential_required": "This action needs a personal credential that is not unlocked. Unlock or reconnect your subscription, then try again.",
|
|
535
|
+
"forbidden": "Your role in this workspace does not allow this action. Ask a workspace admin to do it, or to raise your role.",
|
|
536
|
+
"unavailable": "This deployment has not configured the capability this action needs. Ask your deployment operator to set it up.",
|
|
537
|
+
"unauthorized": "Your session is no longer valid. Sign in again, then retry.",
|
|
538
|
+
"rate_limited": "Too many requests in a short time. Wait a moment, then try again.",
|
|
539
|
+
"internal": "Something went wrong on the server. Try again, and share the details with your deployment operator if it keeps happening.",
|
|
540
|
+
"network": "The server could not be reached. Check your connection, then try again.",
|
|
541
|
+
"unexpected": "The server returned an unexpected response. Try again, and share the details with your deployment operator if it keeps happening.",
|
|
542
|
+
"@unexpected": {
|
|
543
|
+
"description": "Shown when the response was NOT one of our own error shapes at all (a gateway or proxy page, an unrecognised status code). Deliberately distinct from \"internal\", which is our own server reporting its own fault: word this one as an unexpected or unrecognised RESPONSE, not as a server error."
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
},
|
|
518
547
|
"action": {
|
|
519
548
|
"retryFailed": "Retry failed",
|
|
520
549
|
"startFailed": "Failed to start",
|
|
@@ -1344,7 +1373,11 @@
|
|
|
1344
1373
|
"approved": "Approved",
|
|
1345
1374
|
"changes_requested": "Changes requested",
|
|
1346
1375
|
"rejected": "Rejected"
|
|
1347
|
-
}
|
|
1376
|
+
},
|
|
1377
|
+
"promptVariant": "Prompt variant",
|
|
1378
|
+
"promptVariantAdditionOnly": "Only this variant's addition applied. The workspace prompt for this agent replaced its base text.",
|
|
1379
|
+
"promptVariantSuperseded": "This variant did not apply. The workspace prompt for this agent took precedence.",
|
|
1380
|
+
"promptVariantWithdrawn": "This variant is no longer registered, so the step ran the shipped prompt."
|
|
1348
1381
|
},
|
|
1349
1382
|
"stepDetail": {
|
|
1350
1383
|
"contents": "Contents",
|
|
@@ -4129,7 +4162,9 @@
|
|
|
4129
4162
|
"consensusGroups": "Consensus groups",
|
|
4130
4163
|
"consensusGroupsHint": "Pick the reusable panels this step may escalate to. Each carries its own estimate bar; the most demanding one the task clears is the one that runs.",
|
|
4131
4164
|
"consensusGroupsActive": "This step runs the selected group. Deselect every group to configure participants here instead.",
|
|
4132
|
-
"consensusGroupAlways": "always"
|
|
4165
|
+
"consensusGroupAlways": "always",
|
|
4166
|
+
"variantLabel": "Prompt variant",
|
|
4167
|
+
"variantShipped": "Shipped prompt"
|
|
4133
4168
|
},
|
|
4134
4169
|
"progress": {
|
|
4135
4170
|
"status": {
|