@cat-factory/app 0.132.0 → 0.134.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/layout/BoardToolbar.vue +18 -0
- package/app/components/layout/CommandBar.vue +95 -182
- package/app/components/layout/SideBar.vue +18 -266
- package/app/components/panels/StepResultViewHost.vue +48 -86
- package/app/composables/useNavContributions.ts +75 -0
- package/app/modular/agent-kinds.spec.ts +80 -0
- package/app/modular/agent-kinds.ts +83 -0
- package/app/modular/nav-contributions.spec.ts +182 -0
- package/app/modular/nav-contributions.ts +479 -0
- package/app/modular/nav-gates.ts +63 -0
- package/app/modular/registry.spec.ts +90 -7
- package/app/modular/registry.ts +48 -19
- package/app/modular/result-views.ts +104 -0
- package/app/modular/slots.ts +40 -0
- package/app/plugins/modular.client.ts +37 -6
- package/app/stores/agents.spec.ts +100 -0
- package/app/stores/agents.ts +115 -34
- package/app/stores/workspace.ts +4 -3
- package/app/types/domain.ts +4 -3
- package/app/utils/catalog.ts +57 -16
- package/package.json +6 -6
package/app/modular/registry.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { defineModule } from '@modular-vue/core'
|
|
2
1
|
import type { AnyModuleDescriptor } from '@modular-vue/core'
|
|
3
2
|
import { createRegistry } from '@modular-vue/runtime'
|
|
4
3
|
import type { ModuleRegistry } from '@modular-vue/runtime'
|
|
4
|
+
import { navigationModule } from '~/modular/nav-contributions'
|
|
5
|
+
import type { NavGates } from '~/modular/nav-contributions'
|
|
6
|
+
import type { AppSlots } from '~/modular/slots'
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* modular-vue registry for the `@cat-factory/app` layer (slice 0 of the
|
|
@@ -13,21 +15,29 @@ import type { ModuleRegistry } from '@modular-vue/runtime'
|
|
|
13
15
|
* contributes its own, all through the same seam. The registry is resolved and
|
|
14
16
|
* installed by `app/plugins/modular.client.ts`.
|
|
15
17
|
*
|
|
16
|
-
* Slice
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
18
|
+
* Slice 1 registers the first real feature module: `cat-factory:navigation`
|
|
19
|
+
* contributes the whole nav/command catalog to the `nav` slot, gated reactively
|
|
20
|
+
* by a `gates` service + `navSlotFilter` and rendered by `SideBar`, `CommandBar`,
|
|
21
|
+
* and `BoardToolbar` via `useReactiveSlots`. Later slices add result views,
|
|
22
|
+
* wizards, and inspector panels as further modules registered here.
|
|
20
23
|
*/
|
|
21
24
|
|
|
22
25
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
26
|
+
* The layer's shared-dependency shape (grows as later slices wire more deps).
|
|
27
|
+
* A `type` (not `interface`) so it satisfies the registry's
|
|
28
|
+
* `Record<string, any>` dependency constraint — an interface lacks the implicit
|
|
29
|
+
* index signature a type-literal has.
|
|
27
30
|
*/
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
+
export type AppDeps = {
|
|
32
|
+
/** Reactive RBAC/availability gates the nav `slotFilter` reads. */
|
|
33
|
+
gates: NavGates
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* First-party modules the layer always registers. Real feature modules land
|
|
38
|
+
* here as each area is converted; slice 1 adds the navigation catalog.
|
|
39
|
+
*/
|
|
40
|
+
const FIRST_PARTY_MODULES: readonly AnyModuleDescriptor[] = [navigationModule]
|
|
31
41
|
|
|
32
42
|
/**
|
|
33
43
|
* Consumer-contributed modules, collected before the layer resolves its
|
|
@@ -61,14 +71,33 @@ export function __resetConsumerModulesForTest(): void {
|
|
|
61
71
|
}
|
|
62
72
|
|
|
63
73
|
/**
|
|
64
|
-
* Build a fresh registry with the first-party modules
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
74
|
+
* Build a fresh registry with the first-party modules, any `extraModules` the
|
|
75
|
+
* caller supplies, plus everything a consumer contributed via
|
|
76
|
+
* {@link registerAppModule}. A new registry per call because `resolve()` /
|
|
77
|
+
* `resolveManifest()` are single-commit; the install plugin calls this exactly
|
|
78
|
+
* once at client startup (`ssr: false`, so a singleton app).
|
|
79
|
+
*
|
|
80
|
+
* `deps.gates` is the reactive gate service the nav `slotFilter` reads; it's
|
|
81
|
+
* built in the install plugin (Vue context) and registered as a `service` so
|
|
82
|
+
* `useReactiveSlots` tracks it. Each slot default is seeded empty so the key
|
|
83
|
+
* always exists even before any module (or with only consumer modules)
|
|
84
|
+
* contributes to it.
|
|
85
|
+
*
|
|
86
|
+
* `extraModules` is how the client plugin registers first-party modules that
|
|
87
|
+
* carry Vue components (the `resultViews` registry): keeping them out of
|
|
88
|
+
* `FIRST_PARTY_MODULES` keeps this module's static import graph — which the
|
|
89
|
+
* unit tests exercise — free of `.vue` files (the vitest config has no SFC
|
|
90
|
+
* transform).
|
|
68
91
|
*/
|
|
69
|
-
export function createAppRegistry(
|
|
70
|
-
|
|
71
|
-
|
|
92
|
+
export function createAppRegistry(
|
|
93
|
+
deps: AppDeps,
|
|
94
|
+
extraModules: readonly AnyModuleDescriptor[] = [],
|
|
95
|
+
): ModuleRegistry<AppDeps, AppSlots> {
|
|
96
|
+
const registry = createRegistry<AppDeps, AppSlots>({
|
|
97
|
+
services: { gates: deps.gates },
|
|
98
|
+
slots: { nav: [], resultViews: [], agentKinds: [] },
|
|
99
|
+
})
|
|
100
|
+
for (const mod of [...FIRST_PARTY_MODULES, ...extraModules, ...consumerModules]) {
|
|
72
101
|
registry.register(mod)
|
|
73
102
|
}
|
|
74
103
|
return registry
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { Component } from 'vue'
|
|
2
|
+
import { defineModule } from '@modular-vue/core'
|
|
3
|
+
import { RESULT_VIEW_IDS, type ResultViewId } from '@cat-factory/contracts'
|
|
4
|
+
import RequirementsReviewWindow from '~/components/requirements/RequirementsReviewWindow.vue'
|
|
5
|
+
import ClarityReviewWindow from '~/components/clarity/ClarityReviewWindow.vue'
|
|
6
|
+
import BrainstormWindow from '~/components/brainstorm/BrainstormWindow.vue'
|
|
7
|
+
import TestReportWindow from '~/components/testing/TestReportWindow.vue'
|
|
8
|
+
import HumanTestWindow from '~/components/humanTest/HumanTestWindow.vue'
|
|
9
|
+
import VisualConfirmationWindow from '~/components/visualConfirm/VisualConfirmationWindow.vue'
|
|
10
|
+
import GateResultView from '~/components/gates/GateResultView.vue'
|
|
11
|
+
import ConsensusSessionWindow from '~/components/consensus/ConsensusSessionWindow.vue'
|
|
12
|
+
import GenericStructuredResultView from '~/components/panels/GenericStructuredResultView.vue'
|
|
13
|
+
import ServiceSpecWindow from '~/components/spec/ServiceSpecWindow.vue'
|
|
14
|
+
import FollowUpWindow from '~/components/followUp/FollowUpWindow.vue'
|
|
15
|
+
import ForkDecisionWindow from '~/components/forkDecision/ForkDecisionWindow.vue'
|
|
16
|
+
import PrReviewWindow from '~/components/prReview/PrReviewWindow.vue'
|
|
17
|
+
import MergerResultView from '~/components/panels/MergerResultView.vue'
|
|
18
|
+
import InitiativeTrackerWindow from '~/components/initiative/InitiativeTrackerWindow.vue'
|
|
19
|
+
import InitiativePlanningWindow from '~/components/initiative/InitiativePlanningWindow.vue'
|
|
20
|
+
import DocInterviewWindow from '~/components/docs/DocInterviewWindow.vue'
|
|
21
|
+
import RalphLoopResultView from '~/components/ralph/RalphLoopResultView.vue'
|
|
22
|
+
import type { ResultViewContribution } from './slots'
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The first-party result-view registry (slice 2 of the modular-vue adoption —
|
|
26
|
+
* docs/initiatives/modular-vue-adoption.md).
|
|
27
|
+
*
|
|
28
|
+
* Every built-in dedicated result window is contributed as a `ComponentEntry`
|
|
29
|
+
* to the `resultViews` slot instead of living in a hardcoded `Record` in
|
|
30
|
+
* `StepResultViewHost.vue`. The host reads the merged slot via
|
|
31
|
+
* `resolveComponentRegistry` (`@modular-vue/core`) and mounts `get(viewId)`; a
|
|
32
|
+
* step's/kind's `resultView` id (built-in or a custom kind's) selects the entry.
|
|
33
|
+
*
|
|
34
|
+
* A consumer deployment ships its OWN result window by contributing another
|
|
35
|
+
* `ComponentEntry` to the SAME slot from a `registerAppModule` module — it then
|
|
36
|
+
* mounts with zero host edits (the extensibility promise), paired against the
|
|
37
|
+
* kind's `presentation.resultView` id exactly like the built-ins. Consumer ids
|
|
38
|
+
* SHOULD be namespaced (`acme:report`) so they can't collide with a built-in;
|
|
39
|
+
* `resolveComponentRegistry` throws on a duplicate id by default.
|
|
40
|
+
*
|
|
41
|
+
* Because these carry Vue components, this module is registered from the client
|
|
42
|
+
* plugin (`plugins/modular.client.ts`), NOT from `createAppRegistry` — that
|
|
43
|
+
* keeps the pure/unit-tested registry import graph free of `.vue` files (the
|
|
44
|
+
* vitest config has no SFC transform).
|
|
45
|
+
*
|
|
46
|
+
* Built-in coverage is enforced at COMPILE time: {@link BUILT_IN_RESULT_VIEWS} is a
|
|
47
|
+
* `Record<ResultViewId, Component>`, so a built-in added to the contract's
|
|
48
|
+
* `RESULT_VIEW_IDS` picklist without a component here (or a stray id that isn't a
|
|
49
|
+
* built-in) is a `nuxt typecheck` failure — a CI gate — rather than a runtime warning
|
|
50
|
+
* that could ship. Consumer namespaced ids are validated separately by `pairById`.
|
|
51
|
+
*/
|
|
52
|
+
const BUILT_IN_RESULT_VIEWS: Record<ResultViewId, Component> = {
|
|
53
|
+
'requirements-review': RequirementsReviewWindow,
|
|
54
|
+
'clarity-review': ClarityReviewWindow,
|
|
55
|
+
// Shared by both brainstorm stages (requirements + architecture); the window reads the stage.
|
|
56
|
+
brainstorm: BrainstormWindow,
|
|
57
|
+
tester: TestReportWindow,
|
|
58
|
+
// The human-testing gate: env URL + confirm / request-fix / pull-main / recreate / destroy.
|
|
59
|
+
'human-test': HumanTestWindow,
|
|
60
|
+
// The visual-confirmation gate: actual-vs-reference screenshot gallery + approve / request-fix.
|
|
61
|
+
'visual-confirm': VisualConfirmationWindow,
|
|
62
|
+
// Shared by all polling gates (`ci` / `conflicts` / `human-review` / …); the window branches on kind.
|
|
63
|
+
gate: GateResultView,
|
|
64
|
+
// Opened for any step that ran the consensus mechanism (routed in `ui.dispatchStepView`).
|
|
65
|
+
'consensus-session': ConsensusSessionWindow,
|
|
66
|
+
// Default dedicated view for a registered CUSTOM kind's structured (`custom`) output —
|
|
67
|
+
// a read-only JSON viewer, so a proprietary agent ships a result view with no bespoke code.
|
|
68
|
+
'generic-structured': GenericStructuredResultView,
|
|
69
|
+
// The service's prescriptive spec tree (+ Gherkin); opened directly via `ui.openServiceSpec`.
|
|
70
|
+
'service-spec': ServiceSpecWindow,
|
|
71
|
+
// The Follow-up companion: the Coder's surfaced loose ends / questions.
|
|
72
|
+
'follow-ups': FollowUpWindow,
|
|
73
|
+
// The implementation-fork decision: the proposer's approaches + the human's pick / custom.
|
|
74
|
+
'fork-decision': ForkDecisionWindow,
|
|
75
|
+
// The PR deep-review: the reviewer's sliced, prioritized findings + the human's multi-select.
|
|
76
|
+
'pr-review': PrReviewWindow,
|
|
77
|
+
// The merger's verdict: PR complexity/risk/impact scores + the engine's decision (and why).
|
|
78
|
+
merger: MergerResultView,
|
|
79
|
+
// The initiative tracker: phases, per-item status + PR links, decisions, deviations, caveats.
|
|
80
|
+
'initiative-tracker': InitiativeTrackerWindow,
|
|
81
|
+
'initiative-planning': InitiativePlanningWindow,
|
|
82
|
+
// The interactive document-interview gate: clarifying questions + answer / continue / proceed.
|
|
83
|
+
'doc-interview': DocInterviewWindow,
|
|
84
|
+
// The Ralph loop: the retry-until-done iteration history + the validation command + its output.
|
|
85
|
+
'ralph-loop': RalphLoopResultView,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The built-in windows as slot entries, derived from `RESULT_VIEW_IDS` so the slot order matches
|
|
90
|
+
* the canonical id order. Exhaustiveness is guaranteed by {@link BUILT_IN_RESULT_VIEWS}'s type.
|
|
91
|
+
*/
|
|
92
|
+
export const RESULT_VIEW_CONTRIBUTIONS: readonly ResultViewContribution[] = RESULT_VIEW_IDS.map(
|
|
93
|
+
(id) => ({ id, component: BUILT_IN_RESULT_VIEWS[id] }),
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The first-party result-views module: contributes every built-in window to the
|
|
98
|
+
* `resultViews` slot. Registered from the client plugin (see the note above).
|
|
99
|
+
*/
|
|
100
|
+
export const resultViewsModule = defineModule({
|
|
101
|
+
id: 'cat-factory:result-views',
|
|
102
|
+
version: '1.0.0',
|
|
103
|
+
slots: { resultViews: [...RESULT_VIEW_CONTRIBUTIONS] },
|
|
104
|
+
})
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Component } from 'vue'
|
|
2
|
+
import type { ComponentEntry } from '@modular-vue/core'
|
|
3
|
+
import type { CustomAgentKind } from '~/types/domain'
|
|
4
|
+
import type { NavContribution } from './nav-contributions'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The layer's aggregated slot map — the single home for every slot key the
|
|
8
|
+
* first-party modules (and consumer deployments) contribute to. Grows one key
|
|
9
|
+
* per converted seam as the modular-vue adoption proceeds
|
|
10
|
+
* (docs/initiatives/modular-vue-adoption.md):
|
|
11
|
+
*
|
|
12
|
+
* - `nav` (slice 1) — the nav/command catalog, rendered by the three shells.
|
|
13
|
+
* - `resultViews` (slice 2) — the id → dedicated result-window registry
|
|
14
|
+
* ({@link ResultViewContribution}), read by `StepResultViewHost` through
|
|
15
|
+
* `resolveComponentRegistry`. First-party AND consumer components enter here.
|
|
16
|
+
* - `agentKinds` (slice 2) — CODE-shipped custom agent kinds a consumer module
|
|
17
|
+
* contributes (the palette/catalog data half). BACKEND-registered kinds
|
|
18
|
+
* arrive separately as a {@link RemoteModuleManifest} read in the agents
|
|
19
|
+
* store — see `stores/agents.ts`.
|
|
20
|
+
*
|
|
21
|
+
* The index signature is mutable (`unknown[]`) to satisfy the runtime's
|
|
22
|
+
* `SlotMap` constraint while `unknown[]` still meets `useReactiveSlots`'
|
|
23
|
+
* `readonly unknown[]` bound (the slice-1 type-friction note).
|
|
24
|
+
*/
|
|
25
|
+
export interface AppSlots {
|
|
26
|
+
nav: NavContribution[]
|
|
27
|
+
resultViews: ResultViewContribution[]
|
|
28
|
+
agentKinds: CustomAgentKind[]
|
|
29
|
+
[key: string]: unknown[]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* One dedicated result-view window, addressed by its `resultView` id (the same
|
|
34
|
+
* ids as `@cat-factory/contracts` `RESULT_VIEW_IDS` for the built-ins). A plain
|
|
35
|
+
* `ComponentEntry` so the modular `resolveComponentRegistry` / `pairById`
|
|
36
|
+
* helpers index and pair it with the wire-delivered `presentation.resultView`
|
|
37
|
+
* id — the sanctioned "backend data selects a code-shipped, locally-registered
|
|
38
|
+
* component" pairing (see the modular-vue Remote Capability Manifests guide).
|
|
39
|
+
*/
|
|
40
|
+
export type ResultViewContribution = ComponentEntry<Component>
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { installModularApp } from '@modular-vue/nuxt/runtime'
|
|
2
|
+
import { resolveComponentRegistry } from '@modular-vue/core'
|
|
2
3
|
import { createAppRegistry } from '~/modular/registry'
|
|
4
|
+
import { navSlotFilter } from '~/modular/nav-contributions'
|
|
5
|
+
import { createNavGates } from '~/modular/nav-gates'
|
|
6
|
+
import { resultViewsModule } from '~/modular/result-views'
|
|
7
|
+
import type { AppSlots, ResultViewContribution } from '~/modular/slots'
|
|
8
|
+
import type { CustomAgentKind } from '~/types/domain'
|
|
3
9
|
|
|
4
10
|
/**
|
|
5
11
|
* Wire the modular-vue registry into the Nuxt app (slice 0 of the modular-vue
|
|
@@ -16,17 +22,42 @@ import { createAppRegistry } from '~/modular/registry'
|
|
|
16
22
|
*
|
|
17
23
|
* `ssr: false`, so this runs once on the client and a singleton registry is
|
|
18
24
|
* fine. `installModularApp` is the router-owning path: it grafts each module's
|
|
19
|
-
* routes onto Nuxt's router (none yet —
|
|
20
|
-
* installs the modular contexts (shared deps, navigation, slots) app-wide.
|
|
21
|
-
*
|
|
22
|
-
*
|
|
25
|
+
* routes onto Nuxt's router (none yet — the nav modules are non-routed) and
|
|
26
|
+
* installs the modular contexts (shared deps, navigation, slots) app-wide.
|
|
27
|
+
*
|
|
28
|
+
* Slice 1 wires the reactive nav gating here: `createNavGates()` builds the
|
|
29
|
+
* reactive `gates` service (Pinia + composables are available in a `post`
|
|
30
|
+
* plugin), which the registry registers as a `service` and `navSlotFilter` reads
|
|
31
|
+
* per item. The shells consume the gated `nav` slot through `useReactiveSlots`,
|
|
32
|
+
* so a permission/connection flip re-gates them with no `recalculateSlots()`.
|
|
33
|
+
*
|
|
34
|
+
* Slice 2 registers the first-party `resultViews` registry here (via
|
|
35
|
+
* `extraModules`, so its Vue-component imports stay out of the unit-tested
|
|
36
|
+
* `registry.ts` import graph), and feeds the resolved static `agentKinds` slot —
|
|
37
|
+
* the deployment's CODE-shipped consumer agent kinds — into the agents store.
|
|
38
|
+
* (Backend-registered kinds arrive later as the per-workspace capability
|
|
39
|
+
* manifest via `hydrateCustomKinds`.) The result-view components themselves are
|
|
40
|
+
* read reactively by `StepResultViewHost` through `useReactiveSlots`.
|
|
23
41
|
*/
|
|
24
42
|
export default defineNuxtPlugin({
|
|
25
43
|
name: 'cat-factory:modular',
|
|
26
44
|
enforce: 'post',
|
|
27
45
|
setup(nuxtApp) {
|
|
28
|
-
const registry = createAppRegistry()
|
|
29
|
-
const manifest = installModularApp({ vueApp: nuxtApp.vueApp, $router: useRouter() }, registry
|
|
46
|
+
const registry = createAppRegistry({ gates: createNavGates() }, [resultViewsModule])
|
|
47
|
+
const manifest = installModularApp({ vueApp: nuxtApp.vueApp, $router: useRouter() }, registry, {
|
|
48
|
+
slotFilter: navSlotFilter,
|
|
49
|
+
})
|
|
50
|
+
const slots = manifest.slots as AppSlots
|
|
51
|
+
// Fail FAST on a result-view wiring bug (a duplicate id across the first-party +
|
|
52
|
+
// consumer `resultViews` modules) at BOOT rather than lazily the first time a result
|
|
53
|
+
// window opens: resolve the merged slot once here. `resolveComponentRegistry` throws on
|
|
54
|
+
// a duplicate id by default, so a misconfigured deployment surfaces at startup with a
|
|
55
|
+
// clear stack. The slot is static after this resolve, so `StepResultViewHost`'s own
|
|
56
|
+
// reactive re-resolve is a cheap memoized read that this has already validated.
|
|
57
|
+
resolveComponentRegistry((slots.resultViews ?? []) as ResultViewContribution[])
|
|
58
|
+
// Consumer agent kinds contributed as CODE to the static `agentKinds` slot
|
|
59
|
+
// (module slots resolve once, so the static base is the full set).
|
|
60
|
+
useAgentsStore().registerConsumerKinds((slots.agentKinds ?? []) as CustomAgentKind[])
|
|
30
61
|
return { provide: { modular: manifest } }
|
|
31
62
|
},
|
|
32
63
|
})
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { useAgentsStore } from './agents'
|
|
3
|
+
import {
|
|
4
|
+
__resetCustomAgentKindMetaForTest,
|
|
5
|
+
agentKindMeta,
|
|
6
|
+
AGENT_ARCHETYPES,
|
|
7
|
+
isKnownAgentKind,
|
|
8
|
+
} from '~/utils/catalog'
|
|
9
|
+
import type { AgentKind, CustomAgentKind } from '~/types/domain'
|
|
10
|
+
|
|
11
|
+
const backendKind = (
|
|
12
|
+
kind: string,
|
|
13
|
+
over: Partial<CustomAgentKind['presentation']> = {},
|
|
14
|
+
): CustomAgentKind => ({
|
|
15
|
+
kind: kind as AgentKind,
|
|
16
|
+
container: true,
|
|
17
|
+
presentation: {
|
|
18
|
+
label: `L:${kind}`,
|
|
19
|
+
icon: 'i-lucide-shield',
|
|
20
|
+
color: '#abc',
|
|
21
|
+
description: 'd',
|
|
22
|
+
...over,
|
|
23
|
+
},
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
describe('agents store — custom-kind catalog (slice 2)', () => {
|
|
27
|
+
it('exposes only the built-in archetypes before any custom kind is hydrated', () => {
|
|
28
|
+
const agents = useAgentsStore()
|
|
29
|
+
expect(agents.archetypes).toHaveLength(AGENT_ARCHETYPES.length)
|
|
30
|
+
expect(agents.customArchetypes).toEqual([])
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('folds a backend remote-manifest custom kind into the palette + the pure-util projection', () => {
|
|
34
|
+
__resetCustomAgentKindMetaForTest()
|
|
35
|
+
const agents = useAgentsStore()
|
|
36
|
+
// Before hydrate the pure-util lookups don't know the kind.
|
|
37
|
+
expect(isKnownAgentKind('acme-audit')).toBe(false)
|
|
38
|
+
expect(agentKindMeta('acme-audit').label).toBe('Agent') // generic fallback
|
|
39
|
+
|
|
40
|
+
agents.hydrateCustomKinds([
|
|
41
|
+
backendKind('acme-audit', { category: 'review', resultView: 'acme:audit' }),
|
|
42
|
+
])
|
|
43
|
+
|
|
44
|
+
// Palette + kind lookup both see it now (sync-flush projection — no tick needed).
|
|
45
|
+
expect(agents.archetypes.some((a) => a.kind === 'acme-audit')).toBe(true)
|
|
46
|
+
expect(isKnownAgentKind('acme-audit')).toBe(true)
|
|
47
|
+
const meta = agentKindMeta('acme-audit')
|
|
48
|
+
expect(meta.label).toBe('L:acme-audit')
|
|
49
|
+
expect(meta.resultView).toBe('acme:audit')
|
|
50
|
+
expect(meta.category).toBe('review')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('never lets a custom kind shadow a built-in kind', () => {
|
|
54
|
+
const agents = useAgentsStore()
|
|
55
|
+
agents.hydrateCustomKinds([backendKind('coder', { label: 'Evil Coder' })])
|
|
56
|
+
// `coder` is a built-in; the custom entry is dropped, the built-in wins.
|
|
57
|
+
expect(agents.customArchetypes.some((a) => a.kind === 'coder')).toBe(false)
|
|
58
|
+
expect(agentKindMeta('coder').label).not.toBe('Evil Coder')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('merges CODE-shipped consumer kinds with BACKEND manifest kinds, de-duplicated', () => {
|
|
62
|
+
const agents = useAgentsStore()
|
|
63
|
+
agents.registerConsumerKinds([backendKind('acme-consumer')])
|
|
64
|
+
agents.hydrateCustomKinds([backendKind('acme-backend'), backendKind('acme-consumer')])
|
|
65
|
+
const kinds = agents.customArchetypes.map((a) => a.kind)
|
|
66
|
+
expect(kinds).toContain('acme-consumer')
|
|
67
|
+
expect(kinds).toContain('acme-backend')
|
|
68
|
+
// consumer + backend both name `acme-consumer`; it appears once (consumer-slot first).
|
|
69
|
+
expect(kinds.filter((k) => k === 'acme-consumer')).toHaveLength(1)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('no-ops a re-hydrate of identical content (stable projection, content-versioned manifest)', () => {
|
|
73
|
+
const agents = useAgentsStore()
|
|
74
|
+
agents.hydrateCustomKinds([backendKind('acme-x', { resultView: 'acme:x' })])
|
|
75
|
+
const first = agents.customArchetypes
|
|
76
|
+
// A new snapshot re-delivering structurally-equal kinds (fresh objects) must not
|
|
77
|
+
// recompute the merged catalog — the content-derived manifest version is unchanged, so
|
|
78
|
+
// `hydrateCustomKinds` skips the swap and the computed keeps its cached identity.
|
|
79
|
+
agents.hydrateCustomKinds([backendKind('acme-x', { resultView: 'acme:x' })])
|
|
80
|
+
expect(agents.customArchetypes).toBe(first)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('swaps the backend catalog wholesale on re-hydrate (per-workspace manifest)', () => {
|
|
84
|
+
const agents = useAgentsStore()
|
|
85
|
+
agents.hydrateCustomKinds([backendKind('ws1-kind')])
|
|
86
|
+
expect(agents.customArchetypes.some((a) => a.kind === 'ws1-kind')).toBe(true)
|
|
87
|
+
agents.hydrateCustomKinds([backendKind('ws2-kind')])
|
|
88
|
+
expect(agents.customArchetypes.some((a) => a.kind === 'ws1-kind')).toBe(false)
|
|
89
|
+
expect(agents.customArchetypes.some((a) => a.kind === 'ws2-kind')).toBe(true)
|
|
90
|
+
expect(isKnownAgentKind('ws1-kind')).toBe(false)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('addAgent registers an in-UI prototype without touching the built-in catalog', () => {
|
|
94
|
+
const agents = useAgentsStore()
|
|
95
|
+
const created = agents.addAgent({ label: 'My Agent' })
|
|
96
|
+
expect(agents.archetypes.some((a) => a.kind === created.kind)).toBe(true)
|
|
97
|
+
expect(created.category).toBeUndefined() // lands in the palette "custom" bucket
|
|
98
|
+
expect(agentKindMeta(created.kind).label).toBe('My Agent')
|
|
99
|
+
})
|
|
100
|
+
})
|
package/app/stores/agents.ts
CHANGED
|
@@ -1,21 +1,96 @@
|
|
|
1
1
|
import { defineStore } from 'pinia'
|
|
2
|
-
import { ref } from 'vue'
|
|
3
|
-
import {
|
|
2
|
+
import { computed, ref, watch } from 'vue'
|
|
3
|
+
import type { RemoteModuleManifest } from '@modular-vue/core'
|
|
4
|
+
import { buildAgentCapabilitiesManifest, customKindToArchetype } from '~/modular/agent-kinds'
|
|
5
|
+
import type { AppSlots } from '~/modular/slots'
|
|
6
|
+
import {
|
|
7
|
+
AGENT_ARCHETYPES,
|
|
8
|
+
AGENT_BY_KIND,
|
|
9
|
+
setCustomAgentKindMeta,
|
|
10
|
+
SYSTEM_AGENT_META,
|
|
11
|
+
uid,
|
|
12
|
+
} from '~/utils/catalog'
|
|
4
13
|
import type { AgentArchetype, AgentKind, CustomAgentKind } from '~/types/domain'
|
|
5
14
|
|
|
6
15
|
/**
|
|
7
|
-
* The agent palette
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
16
|
+
* The agent palette catalog (slice 2 of the modular-vue adoption —
|
|
17
|
+
* docs/initiatives/modular-vue-adoption.md).
|
|
18
|
+
*
|
|
19
|
+
* Reactive union of three sources, none of which mutates the frozen built-in
|
|
20
|
+
* {@link AGENT_BY_KIND} const any more:
|
|
21
|
+
* - the built-in archetypes (static);
|
|
22
|
+
* - CONSUMER-shipped kinds contributed as CODE via the modular `agentKinds`
|
|
23
|
+
* slot (`registerConsumerKinds`, fed once at boot from the resolved manifest);
|
|
24
|
+
* - the deployment's BACKEND-registered kinds, modeled as a single
|
|
25
|
+
* {@link RemoteModuleManifest} swapped per workspace snapshot
|
|
26
|
+
* (`hydrateCustomKinds`) and read directly (single-active-manifest shape).
|
|
27
|
+
*
|
|
28
|
+
* The merged custom catalog is projected back into `catalog.ts`'s
|
|
29
|
+
* {@link setCustomAgentKindMeta} read-model so the pure `agentKindMeta` /
|
|
30
|
+
* `isKnownAgentKind` lookups (used across ~17 renderers) resolve a custom kind
|
|
31
|
+
* reactively without importing this store.
|
|
11
32
|
*/
|
|
12
33
|
export const useAgentsStore = defineStore('agents', () => {
|
|
13
|
-
|
|
34
|
+
// CODE-shipped consumer kinds from the static `agentKinds` slot (fed once at
|
|
35
|
+
// boot by the modular install plugin — module slots are resolved once).
|
|
36
|
+
const consumerKinds = ref<CustomAgentKind[]>([])
|
|
37
|
+
// The active per-workspace capability manifest built from the snapshot's
|
|
38
|
+
// `customAgentKinds`, or null before the first hydrate.
|
|
39
|
+
const capabilitiesManifest = ref<RemoteModuleManifest<AppSlots> | null>(null)
|
|
40
|
+
// In-UI, client-only prototype agents created via the "add agent" modal.
|
|
41
|
+
const runtimeAgents = ref<AgentArchetype[]>([])
|
|
14
42
|
|
|
15
|
-
|
|
16
|
-
|
|
43
|
+
/**
|
|
44
|
+
* The merged CUSTOM catalog (consumer-slot → backend-manifest → runtime), each
|
|
45
|
+
* mapped to display metadata, de-duplicated, and never shadowing a built-in or
|
|
46
|
+
* system kind. The old `registerCustomKinds` only guarded `AGENT_BY_KIND`; this
|
|
47
|
+
* intentionally ALSO drops any custom kind colliding with a `SYSTEM_AGENT_META`
|
|
48
|
+
* kind (`ci` / `merger` / `blueprints` / gates …), so a snapshot can't override an
|
|
49
|
+
* engine kind's palette entry either — matching `agentKindMeta`'s precedence
|
|
50
|
+
* (built-in → system → custom), where a colliding custom kind would never win anyway.
|
|
51
|
+
*/
|
|
52
|
+
const customArchetypes = computed<AgentArchetype[]>(() => {
|
|
53
|
+
const seen = new Set<string>()
|
|
54
|
+
const out: AgentArchetype[] = []
|
|
55
|
+
const add = (a: AgentArchetype) => {
|
|
56
|
+
if (a.kind in AGENT_BY_KIND || a.kind in SYSTEM_AGENT_META || seen.has(a.kind)) return
|
|
57
|
+
seen.add(a.kind)
|
|
58
|
+
out.push(a)
|
|
59
|
+
}
|
|
60
|
+
for (const k of consumerKinds.value) add(customKindToArchetype(k))
|
|
61
|
+
for (const k of capabilitiesManifest.value?.slots?.agentKinds ?? [])
|
|
62
|
+
add(customKindToArchetype(k))
|
|
63
|
+
for (const a of runtimeAgents.value) add(a)
|
|
64
|
+
return out
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
/** The full palette: built-in archetypes + the merged custom ones. */
|
|
68
|
+
const archetypes = computed<AgentArchetype[]>(() => [
|
|
69
|
+
...AGENT_ARCHETYPES,
|
|
70
|
+
...customArchetypes.value,
|
|
71
|
+
])
|
|
72
|
+
|
|
73
|
+
// Known-kind lookup (built-in ∪ system ∪ custom) for `get`.
|
|
74
|
+
const customByKind = computed<Record<string, AgentArchetype>>(() =>
|
|
75
|
+
Object.fromEntries(customArchetypes.value.map((a) => [a.kind, a])),
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
// Keep `catalog.ts`'s pure-util projection in sync with the merged custom
|
|
79
|
+
// catalog so `agentKindMeta` / `isKnownAgentKind` resolve custom kinds. Sync
|
|
80
|
+
// flush so an imperative read right after `hydrateCustomKinds` (e.g. the run
|
|
81
|
+
// dispatch resolving a custom kind's `resultView`) sees the fresh catalog with
|
|
82
|
+
// no tick gap. The watch lives in the store's effect scope (disposed with it).
|
|
83
|
+
watch(customByKind, (map) => setCustomAgentKindMeta(map), { immediate: true, flush: 'sync' })
|
|
84
|
+
|
|
85
|
+
/** Display metadata for a KNOWN kind (built-in / system / custom), else undefined. */
|
|
86
|
+
function get(kind: AgentKind): AgentArchetype | undefined {
|
|
87
|
+
return AGENT_BY_KIND[kind] ?? SYSTEM_AGENT_META[kind] ?? customByKind.value[kind]
|
|
17
88
|
}
|
|
18
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Add an in-UI prototype agent (the pipeline builder's "add agent" modal).
|
|
92
|
+
* Client-only, so it lives in store state — no backend, no global mutation.
|
|
93
|
+
*/
|
|
19
94
|
function addAgent(input: {
|
|
20
95
|
label: string
|
|
21
96
|
description?: string
|
|
@@ -30,36 +105,42 @@ export const useAgentsStore = defineStore('agents', () => {
|
|
|
30
105
|
icon: input.icon || 'i-lucide-sparkles',
|
|
31
106
|
color: input.color || '#22d3ee',
|
|
32
107
|
}
|
|
33
|
-
|
|
34
|
-
AGENT_BY_KIND[archetype.kind] = archetype
|
|
35
|
-
archetypes.value.push(archetype)
|
|
108
|
+
runtimeAgents.value = [...runtimeAgents.value, archetype]
|
|
36
109
|
return archetype
|
|
37
110
|
}
|
|
38
111
|
|
|
39
112
|
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* declared `resultView` opens through the same registry the built-ins use. Idempotent
|
|
44
|
-
* and built-in-safe — a kind already known (a built-in, or a prior load) is left
|
|
45
|
-
* untouched, so a snapshot can't shadow a built-in or duplicate on reload.
|
|
113
|
+
* Register the deployment's CODE-shipped consumer agent kinds — the resolved
|
|
114
|
+
* modular `agentKinds` slot, fed once by the install plugin. Idempotent
|
|
115
|
+
* replace (module slots resolve once, so this is called a single time).
|
|
46
116
|
*/
|
|
47
|
-
function
|
|
48
|
-
|
|
49
|
-
if (AGENT_BY_KIND[kind]) continue
|
|
50
|
-
const archetype: AgentArchetype = {
|
|
51
|
-
kind,
|
|
52
|
-
label: presentation.label,
|
|
53
|
-
icon: presentation.icon,
|
|
54
|
-
color: presentation.color,
|
|
55
|
-
description: presentation.description,
|
|
56
|
-
...(presentation.category ? { category: presentation.category } : {}),
|
|
57
|
-
...(presentation.resultView ? { resultView: presentation.resultView } : {}),
|
|
58
|
-
}
|
|
59
|
-
AGENT_BY_KIND[kind] = archetype
|
|
60
|
-
archetypes.value.push(archetype)
|
|
61
|
-
}
|
|
117
|
+
function registerConsumerKinds(kinds: readonly CustomAgentKind[]) {
|
|
118
|
+
consumerKinds.value = [...kinds]
|
|
62
119
|
}
|
|
63
120
|
|
|
64
|
-
|
|
121
|
+
/**
|
|
122
|
+
* Hydrate the deployment's BACKEND-registered custom kinds from a workspace
|
|
123
|
+
* snapshot, modeled as a single remote capability manifest (swapped wholesale
|
|
124
|
+
* per workspace). Replaces the old `registerCustomKinds` that mutated
|
|
125
|
+
* {@link AGENT_BY_KIND} directly.
|
|
126
|
+
*
|
|
127
|
+
* The snapshot re-delivers the same deployment kinds on every board refresh, so
|
|
128
|
+
* skip the swap — and the downstream projection invalidation of every
|
|
129
|
+
* `agentKindMeta` consumer — when the content-derived manifest version is
|
|
130
|
+
* unchanged. A genuinely different workspace's kinds change the version and swap.
|
|
131
|
+
*/
|
|
132
|
+
function hydrateCustomKinds(kinds: CustomAgentKind[]) {
|
|
133
|
+
const next = buildAgentCapabilitiesManifest(kinds)
|
|
134
|
+
if (capabilitiesManifest.value?.version === next.version) return
|
|
135
|
+
capabilitiesManifest.value = next
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
archetypes,
|
|
140
|
+
customArchetypes,
|
|
141
|
+
get,
|
|
142
|
+
addAgent,
|
|
143
|
+
registerConsumerKinds,
|
|
144
|
+
hydrateCustomKinds,
|
|
145
|
+
}
|
|
65
146
|
})
|
package/app/stores/workspace.ts
CHANGED
|
@@ -168,9 +168,10 @@ export const useWorkspaceStore = defineStore(
|
|
|
168
168
|
useInitiativesStore().hydratePresets(snapshot.initiativePresets)
|
|
169
169
|
useTrackerStore().hydrate(snapshot.trackerSettings)
|
|
170
170
|
useServicesStore().hydrate(snapshot.mounts ?? [], snapshot.serviceCatalog ?? [])
|
|
171
|
-
//
|
|
172
|
-
// proprietary kind renders as a first-class
|
|
173
|
-
|
|
171
|
+
// Hydrate the deployment's backend-registered custom agent kinds as the workspace's
|
|
172
|
+
// remote capability manifest, so a proprietary kind renders as a first-class palette
|
|
173
|
+
// block + result view. Swapped wholesale per workspace (no global-catalog mutation).
|
|
174
|
+
useAgentsStore().hydrateCustomKinds(snapshot.customAgentKinds ?? [])
|
|
174
175
|
// The account's repo-sourced Claude Skills catalog (shared across its workspaces), so the
|
|
175
176
|
// pipeline builder's per-step skill picker has its options. A straight replace.
|
|
176
177
|
useSkillsStore().hydrate(snapshot.skills ?? [])
|
package/app/types/domain.ts
CHANGED
|
@@ -108,9 +108,10 @@ export interface AgentArchetype {
|
|
|
108
108
|
category?: AgentCategory
|
|
109
109
|
/**
|
|
110
110
|
* Optional id of a DEDICATED result window this agent's step opens instead of the
|
|
111
|
-
* generic prose step-detail panel. Resolved through the
|
|
112
|
-
* (`
|
|
113
|
-
*
|
|
111
|
+
* generic prose step-detail panel. Resolved through the modular `resultViews` slot
|
|
112
|
+
* registry (`~/modular/result-views`, read by `StepResultViewHost`) so any agent —
|
|
113
|
+
* built-in or a consumer's — can declare a bespoke visualization without the renderer
|
|
114
|
+
* hardcoding a kind. Absent → the generic `AgentStepDetail` panel.
|
|
114
115
|
*/
|
|
115
116
|
resultView?: string
|
|
116
117
|
}
|