@cat-factory/app 0.147.7 → 0.149.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 +146 -3
- package/app/components/board/nodes/TaskCard.vue +29 -1
- package/app/components/fragments/FragmentLibraryManager.vue +188 -18
- package/app/components/panels/AgentStepDetail.vue +11 -0
- package/app/components/panels/StepEffortReport.vue +86 -0
- package/app/components/panels/StepFragmentAdherence.vue +85 -0
- package/app/components/prReview/PrReviewWindow.vue +8 -0
- package/app/components/settings/WorkspaceSettingsPanel.vue +14 -8
- package/app/composables/api/fragments.ts +17 -0
- package/app/docs/consumer-extensions.md +41 -8
- package/app/modular/agent-kinds.spec.ts +1 -39
- package/app/modular/agent-kinds.ts +7 -58
- package/app/modular/capabilities.spec.ts +88 -0
- package/app/modular/capabilities.ts +77 -0
- package/app/modular/nav-contributions.spec.ts +2 -0
- package/app/modular/registry.ts +8 -1
- package/app/modular/slots.ts +13 -1
- package/app/modular/task-types.ts +27 -0
- package/app/plugins/modular.client.ts +8 -3
- package/app/stores/agents.spec.ts +13 -9
- package/app/stores/agents.ts +16 -17
- package/app/stores/fragmentLibrary.ts +16 -0
- package/app/stores/taskTypes.spec.ts +100 -0
- package/app/stores/taskTypes.ts +88 -0
- package/app/stores/workspace.ts +14 -4
- package/app/types/domain.ts +23 -0
- package/app/types/execution.ts +3 -0
- package/app/types/fragments.ts +2 -0
- package/app/utils/catalog.ts +107 -0
- package/i18n/locales/de.json +23 -4
- package/i18n/locales/en.json +23 -4
- package/i18n/locales/es.json +23 -4
- package/i18n/locales/fr.json +23 -4
- package/i18n/locales/he.json +23 -4
- package/i18n/locales/it.json +23 -4
- package/i18n/locales/ja.json +23 -4
- package/i18n/locales/pl.json +23 -4
- package/i18n/locales/tr.json +23 -4
- package/i18n/locales/uk.json +23 -4
- package/package.json +2 -2
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
environmentSetupPersistence,
|
|
17
17
|
} from '~/modular/journeys/environmentSetup'
|
|
18
18
|
import type { AppSlots, ResultViewContribution } from '~/modular/slots'
|
|
19
|
-
import type { Block, CustomAgentKind } from '~/types/domain'
|
|
19
|
+
import type { Block, CustomAgentKind, CustomTaskType } from '~/types/domain'
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* Wire the modular-vue registry into the Nuxt app (slice 0 of the modular-vue
|
|
@@ -98,9 +98,14 @@ export default defineNuxtPlugin({
|
|
|
98
98
|
// selected. `<PanelsOutlet>` re-resolves reactively per subject; this only
|
|
99
99
|
// validates the wiring.
|
|
100
100
|
resolvePanels((slots.inspectorPanels ?? []) as PanelEntry<Block>[], null)
|
|
101
|
-
//
|
|
102
|
-
//
|
|
101
|
+
// Same fail-fast for the custom-task-type form-panel registry (extension slice B): a
|
|
102
|
+
// duplicate `formPanel` id across the first-party + consumer modules throws at BOOT
|
|
103
|
+
// rather than the first time the create-task form opens a custom type's section.
|
|
104
|
+
resolveComponentRegistry((slots.taskTypeFormPanels ?? []) as ResultViewContribution[])
|
|
105
|
+
// Consumer agent kinds + task types contributed as CODE to the static `agentKinds` /
|
|
106
|
+
// `taskTypes` slots (module slots resolve once, so the static base is the full set).
|
|
103
107
|
useAgentsStore().registerConsumerKinds((slots.agentKinds ?? []) as CustomAgentKind[])
|
|
108
|
+
useTaskTypesStore().registerConsumerTaskTypes((slots.taskTypes ?? []) as CustomTaskType[])
|
|
104
109
|
return { provide: { modular: manifest } }
|
|
105
110
|
},
|
|
106
111
|
})
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
2
|
import { useAgentsStore } from './agents'
|
|
3
|
+
import { buildWorkspaceCapabilitiesManifest } from '~/modular/capabilities'
|
|
3
4
|
import {
|
|
4
5
|
__resetCustomAgentKindMetaForTest,
|
|
5
6
|
agentKindMeta,
|
|
@@ -23,6 +24,11 @@ const backendKind = (
|
|
|
23
24
|
},
|
|
24
25
|
})
|
|
25
26
|
|
|
27
|
+
/** Hydrate the store's backend-manifest half with `kinds` (via the shared capability manifest). */
|
|
28
|
+
const hydrate = (agents: ReturnType<typeof useAgentsStore>, kinds: CustomAgentKind[]): void => {
|
|
29
|
+
agents.hydrateCapabilities(buildWorkspaceCapabilitiesManifest(kinds, []))
|
|
30
|
+
}
|
|
31
|
+
|
|
26
32
|
describe('agents store — custom-kind catalog (slice 2)', () => {
|
|
27
33
|
it('exposes only the built-in archetypes before any custom kind is hydrated', () => {
|
|
28
34
|
const agents = useAgentsStore()
|
|
@@ -37,9 +43,7 @@ describe('agents store — custom-kind catalog (slice 2)', () => {
|
|
|
37
43
|
expect(isKnownAgentKind('acme-audit')).toBe(false)
|
|
38
44
|
expect(agentKindMeta('acme-audit').label).toBe('Agent') // generic fallback
|
|
39
45
|
|
|
40
|
-
agents
|
|
41
|
-
backendKind('acme-audit', { category: 'review', resultView: 'acme:audit' }),
|
|
42
|
-
])
|
|
46
|
+
hydrate(agents, [backendKind('acme-audit', { category: 'review', resultView: 'acme:audit' })])
|
|
43
47
|
|
|
44
48
|
// Palette + kind lookup both see it now (sync-flush projection — no tick needed).
|
|
45
49
|
expect(agents.archetypes.some((a) => a.kind === 'acme-audit')).toBe(true)
|
|
@@ -52,7 +56,7 @@ describe('agents store — custom-kind catalog (slice 2)', () => {
|
|
|
52
56
|
|
|
53
57
|
it('never lets a custom kind shadow a built-in kind', () => {
|
|
54
58
|
const agents = useAgentsStore()
|
|
55
|
-
agents
|
|
59
|
+
hydrate(agents, [backendKind('coder', { label: 'Evil Coder' })])
|
|
56
60
|
// `coder` is a built-in; the custom entry is dropped, the built-in wins.
|
|
57
61
|
expect(agents.customArchetypes.some((a) => a.kind === 'coder')).toBe(false)
|
|
58
62
|
expect(agentKindMeta('coder').label).not.toBe('Evil Coder')
|
|
@@ -61,7 +65,7 @@ describe('agents store — custom-kind catalog (slice 2)', () => {
|
|
|
61
65
|
it('merges CODE-shipped consumer kinds with BACKEND manifest kinds, de-duplicated', () => {
|
|
62
66
|
const agents = useAgentsStore()
|
|
63
67
|
agents.registerConsumerKinds([backendKind('acme-consumer')])
|
|
64
|
-
agents
|
|
68
|
+
hydrate(agents, [backendKind('acme-backend'), backendKind('acme-consumer')])
|
|
65
69
|
const kinds = agents.customArchetypes.map((a) => a.kind)
|
|
66
70
|
expect(kinds).toContain('acme-consumer')
|
|
67
71
|
expect(kinds).toContain('acme-backend')
|
|
@@ -71,20 +75,20 @@ describe('agents store — custom-kind catalog (slice 2)', () => {
|
|
|
71
75
|
|
|
72
76
|
it('no-ops a re-hydrate of identical content (stable projection, content-versioned manifest)', () => {
|
|
73
77
|
const agents = useAgentsStore()
|
|
74
|
-
agents
|
|
78
|
+
hydrate(agents, [backendKind('acme-x', { resultView: 'acme:x' })])
|
|
75
79
|
const first = agents.customArchetypes
|
|
76
80
|
// A new snapshot re-delivering structurally-equal kinds (fresh objects) must not
|
|
77
81
|
// recompute the merged catalog — the content-derived manifest version is unchanged, so
|
|
78
82
|
// `hydrateCustomKinds` skips the swap and the computed keeps its cached identity.
|
|
79
|
-
agents
|
|
83
|
+
hydrate(agents, [backendKind('acme-x', { resultView: 'acme:x' })])
|
|
80
84
|
expect(agents.customArchetypes).toBe(first)
|
|
81
85
|
})
|
|
82
86
|
|
|
83
87
|
it('swaps the backend catalog wholesale on re-hydrate (per-workspace manifest)', () => {
|
|
84
88
|
const agents = useAgentsStore()
|
|
85
|
-
agents
|
|
89
|
+
hydrate(agents, [backendKind('ws1-kind')])
|
|
86
90
|
expect(agents.customArchetypes.some((a) => a.kind === 'ws1-kind')).toBe(true)
|
|
87
|
-
agents
|
|
91
|
+
hydrate(agents, [backendKind('ws2-kind')])
|
|
88
92
|
expect(agents.customArchetypes.some((a) => a.kind === 'ws1-kind')).toBe(false)
|
|
89
93
|
expect(agents.customArchetypes.some((a) => a.kind === 'ws2-kind')).toBe(true)
|
|
90
94
|
expect(isKnownAgentKind('ws1-kind')).toBe(false)
|
package/app/stores/agents.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { defineStore } from 'pinia'
|
|
2
2
|
import { computed, ref, watch } from 'vue'
|
|
3
3
|
import type { RemoteModuleManifest } from '@modular-vue/core'
|
|
4
|
-
import {
|
|
4
|
+
import { customKindToArchetype } from '~/modular/agent-kinds'
|
|
5
5
|
import type { AppSlots } from '~/modular/slots'
|
|
6
6
|
import {
|
|
7
7
|
AGENT_ARCHETYPES,
|
|
@@ -21,9 +21,9 @@ import type { AgentArchetype, AgentKind, CustomAgentKind } from '~/types/domain'
|
|
|
21
21
|
* - the built-in archetypes (static);
|
|
22
22
|
* - CONSUMER-shipped kinds contributed as CODE via the modular `agentKinds`
|
|
23
23
|
* slot (`registerConsumerKinds`, fed once at boot from the resolved manifest);
|
|
24
|
-
* - the deployment's BACKEND-registered kinds,
|
|
24
|
+
* - the deployment's BACKEND-registered kinds, read from the shared per-workspace
|
|
25
25
|
* {@link RemoteModuleManifest} swapped per workspace snapshot
|
|
26
|
-
* (`
|
|
26
|
+
* (`hydrateCapabilities`, reading its own `agentKinds` slot — single-active-manifest shape).
|
|
27
27
|
*
|
|
28
28
|
* The merged custom catalog is projected back into `catalog.ts`'s
|
|
29
29
|
* {@link setCustomAgentKindMeta} read-model so the pure `agentKindMeta` /
|
|
@@ -77,7 +77,7 @@ export const useAgentsStore = defineStore('agents', () => {
|
|
|
77
77
|
|
|
78
78
|
// Keep `catalog.ts`'s pure-util projection in sync with the merged custom
|
|
79
79
|
// catalog so `agentKindMeta` / `isKnownAgentKind` resolve custom kinds. Sync
|
|
80
|
-
// flush so an imperative read right after `
|
|
80
|
+
// flush so an imperative read right after `hydrateCapabilities` (e.g. the run
|
|
81
81
|
// dispatch resolving a custom kind's `resultView`) sees the fresh catalog with
|
|
82
82
|
// no tick gap. The watch lives in the store's effect scope (disposed with it).
|
|
83
83
|
watch(customByKind, (map) => setCustomAgentKindMeta(map), { immediate: true, flush: 'sync' })
|
|
@@ -119,20 +119,19 @@ export const useAgentsStore = defineStore('agents', () => {
|
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
/**
|
|
122
|
-
* Hydrate the deployment's BACKEND-registered custom kinds from
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
* {@link AGENT_BY_KIND} directly.
|
|
122
|
+
* Hydrate the deployment's BACKEND-registered custom kinds from the shared per-workspace
|
|
123
|
+
* capability manifest (built by the workspace store from the snapshot, carrying both `agentKinds`
|
|
124
|
+
* + `taskTypes`; this store reads only its own `agentKinds` slot). Swapped wholesale per
|
|
125
|
+
* workspace. Replaces the old `registerCustomKinds` that mutated {@link AGENT_BY_KIND} directly.
|
|
126
126
|
*
|
|
127
|
-
* The snapshot re-delivers the same deployment kinds on every board refresh, so
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
127
|
+
* The snapshot re-delivers the same deployment kinds on every board refresh, so skip the swap —
|
|
128
|
+
* and the downstream projection invalidation of every `agentKindMeta` consumer — when the
|
|
129
|
+
* content-derived manifest version is unchanged. A genuinely different workspace's capabilities
|
|
130
|
+
* change the version and swap.
|
|
131
131
|
*/
|
|
132
|
-
function
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
capabilitiesManifest.value = next
|
|
132
|
+
function hydrateCapabilities(manifest: RemoteModuleManifest<AppSlots>) {
|
|
133
|
+
if (capabilitiesManifest.value?.version === manifest.version) return
|
|
134
|
+
capabilitiesManifest.value = manifest
|
|
136
135
|
}
|
|
137
136
|
|
|
138
137
|
return {
|
|
@@ -141,6 +140,6 @@ export const useAgentsStore = defineStore('agents', () => {
|
|
|
141
140
|
get,
|
|
142
141
|
addAgent,
|
|
143
142
|
registerConsumerKinds,
|
|
144
|
-
|
|
143
|
+
hydrateCapabilities,
|
|
145
144
|
}
|
|
146
145
|
})
|
|
@@ -111,6 +111,21 @@ function fragmentLibrarySetup(kind: FragmentOwnerKind, resolveOwnerId: () => str
|
|
|
111
111
|
await Promise.all([reloadTier(), refreshResolved()])
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Auto-generate a title for a fragment from its content (an inline LLM call). Returns the
|
|
116
|
+
* suggested title for the caller to drop into the editor; performs no write. At the account
|
|
117
|
+
* scope the model resolves against `viaWorkspaceId`'s credential scope.
|
|
118
|
+
*/
|
|
119
|
+
async function generateTitle(input: { body: string; summary?: string }): Promise<string> {
|
|
120
|
+
const { title } = await api.generateFragmentTitle(
|
|
121
|
+
kind,
|
|
122
|
+
requireOwnerId(),
|
|
123
|
+
input,
|
|
124
|
+
viaWorkspaceId.value,
|
|
125
|
+
)
|
|
126
|
+
return title
|
|
127
|
+
}
|
|
128
|
+
|
|
114
129
|
/** Link an external document as a living (dynamically-resolved) fragment. */
|
|
115
130
|
async function createDocumentFragment(input: CreateDocumentFragmentInput) {
|
|
116
131
|
loading.value = true
|
|
@@ -203,6 +218,7 @@ function fragmentLibrarySetup(kind: FragmentOwnerKind, resolveOwnerId: () => str
|
|
|
203
218
|
createDocumentFragment,
|
|
204
219
|
refreshDocumentFragment,
|
|
205
220
|
update,
|
|
221
|
+
generateTitle,
|
|
206
222
|
remove,
|
|
207
223
|
linkSource,
|
|
208
224
|
unlinkSource,
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { useTaskTypesStore } from './taskTypes'
|
|
3
|
+
import { buildWorkspaceCapabilitiesManifest } from '~/modular/capabilities'
|
|
4
|
+
import { __resetCustomTaskTypeMetaForTest, isKnownTaskType, taskTypeMeta } from '~/utils/catalog'
|
|
5
|
+
import type { CustomTaskType } from '~/types/domain'
|
|
6
|
+
|
|
7
|
+
const backendType = (
|
|
8
|
+
taskType: string,
|
|
9
|
+
over: Partial<CustomTaskType['presentation']> = {},
|
|
10
|
+
): CustomTaskType => ({
|
|
11
|
+
taskType,
|
|
12
|
+
presentation: {
|
|
13
|
+
label: `L:${taskType}`,
|
|
14
|
+
icon: 'i-lucide-siren',
|
|
15
|
+
color: '#ef4444',
|
|
16
|
+
description: 'd',
|
|
17
|
+
...over,
|
|
18
|
+
},
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
/** Hydrate the store's backend-manifest half with `taskTypes` (via the shared capability manifest). */
|
|
22
|
+
const hydrate = (
|
|
23
|
+
store: ReturnType<typeof useTaskTypesStore>,
|
|
24
|
+
taskTypes: CustomTaskType[],
|
|
25
|
+
): void => {
|
|
26
|
+
store.hydrateCapabilities(buildWorkspaceCapabilitiesManifest([], taskTypes))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('taskTypes store — custom task-type catalog (extension slice B)', () => {
|
|
30
|
+
it('exposes no custom types before any hydrate', () => {
|
|
31
|
+
const store = useTaskTypesStore()
|
|
32
|
+
expect(store.customTaskTypes).toEqual([])
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('folds a backend remote-manifest custom type into the catalog + the pure-util projection', () => {
|
|
36
|
+
__resetCustomTaskTypeMetaForTest()
|
|
37
|
+
const store = useTaskTypesStore()
|
|
38
|
+
// Before hydrate the pure-util lookups don't know the type; taskTypeMeta degrades to `feature`.
|
|
39
|
+
expect(isKnownTaskType('acme:incident')).toBe(false)
|
|
40
|
+
const before = taskTypeMeta('acme:incident')
|
|
41
|
+
expect(before.icon).toBe(taskTypeMeta('feature').icon) // feature fallback icon
|
|
42
|
+
expect(before.label).toBe('acme:incident') // raw id, never blank
|
|
43
|
+
|
|
44
|
+
hydrate(store, [backendType('acme:incident', { label: 'Incident' })])
|
|
45
|
+
|
|
46
|
+
// Catalog + lookup both see it now (sync-flush projection — no tick needed).
|
|
47
|
+
expect(store.customTaskTypes.some((t) => t.taskType === 'acme:incident')).toBe(true)
|
|
48
|
+
expect(isKnownTaskType('acme:incident')).toBe(true)
|
|
49
|
+
const meta = taskTypeMeta('acme:incident')
|
|
50
|
+
expect(meta.label).toBe('Incident')
|
|
51
|
+
expect(meta.labelKey).toBeUndefined() // custom types carry a literal label, not an i18n key
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('never lets a custom type shadow a built-in type', () => {
|
|
55
|
+
const store = useTaskTypesStore()
|
|
56
|
+
hydrate(store, [backendType('feature', { label: 'Evil Feature' })])
|
|
57
|
+
expect(store.customTaskTypes.some((t) => t.taskType === 'feature')).toBe(false)
|
|
58
|
+
expect(taskTypeMeta('feature').labelKey).toBe('board.addTask.types.feature')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('merges CODE-shipped consumer types with BACKEND manifest types, de-duplicated', () => {
|
|
62
|
+
const store = useTaskTypesStore()
|
|
63
|
+
store.registerConsumerTaskTypes([backendType('acme:consumer')])
|
|
64
|
+
hydrate(store, [backendType('acme:backend'), backendType('acme:consumer')])
|
|
65
|
+
const ids = store.customTaskTypes.map((t) => t.taskType)
|
|
66
|
+
expect(ids).toContain('acme:consumer')
|
|
67
|
+
expect(ids).toContain('acme:backend')
|
|
68
|
+
expect(ids.filter((k) => k === 'acme:consumer')).toHaveLength(1)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('no-ops a re-hydrate of identical content (content-versioned manifest)', () => {
|
|
72
|
+
const store = useTaskTypesStore()
|
|
73
|
+
hydrate(store, [backendType('acme:x')])
|
|
74
|
+
const first = store.customTaskTypes
|
|
75
|
+
hydrate(store, [backendType('acme:x')])
|
|
76
|
+
expect(store.customTaskTypes).toBe(first)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('swaps the backend catalog wholesale on re-hydrate (per-workspace manifest)', () => {
|
|
80
|
+
const store = useTaskTypesStore()
|
|
81
|
+
hydrate(store, [backendType('acme:ws1')])
|
|
82
|
+
expect(store.customTaskTypes.some((t) => t.taskType === 'acme:ws1')).toBe(true)
|
|
83
|
+
hydrate(store, [backendType('acme:ws2')])
|
|
84
|
+
expect(store.customTaskTypes.some((t) => t.taskType === 'acme:ws1')).toBe(false)
|
|
85
|
+
expect(store.customTaskTypes.some((t) => t.taskType === 'acme:ws2')).toBe(true)
|
|
86
|
+
expect(isKnownTaskType('acme:ws1')).toBe(false)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('get() returns the full registration (for the create-form field descriptors)', () => {
|
|
90
|
+
const store = useTaskTypesStore()
|
|
91
|
+
hydrate(store, [
|
|
92
|
+
{
|
|
93
|
+
...backendType('acme:incident'),
|
|
94
|
+
fields: [{ key: 'sev', label: 'Severity', type: 'text' }],
|
|
95
|
+
},
|
|
96
|
+
])
|
|
97
|
+
expect(store.get('acme:incident')?.fields?.[0]?.key).toBe('sev')
|
|
98
|
+
expect(store.get('acme:unknown')).toBeUndefined()
|
|
99
|
+
})
|
|
100
|
+
})
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { defineStore } from 'pinia'
|
|
2
|
+
import { computed, ref, watch } from 'vue'
|
|
3
|
+
import type { RemoteModuleManifest } from '@modular-vue/core'
|
|
4
|
+
import { customTaskTypeToMeta } from '~/modular/task-types'
|
|
5
|
+
import type { AppSlots } from '~/modular/slots'
|
|
6
|
+
import { TASK_TYPE_META, setCustomTaskTypeMeta } from '~/utils/catalog'
|
|
7
|
+
import type { CustomTaskType, TaskTypeMeta } from '~/types/domain'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The custom task-type catalog (frontend-extension-mechanism initiative, slice B) — the exact twin
|
|
11
|
+
* of the agents store (`stores/agents.ts`), which is the template for every capability catalog.
|
|
12
|
+
*
|
|
13
|
+
* Reactive union of two sources, neither mutating the built-in {@link TASK_TYPE_META} const:
|
|
14
|
+
* - CONSUMER-shipped task types contributed as CODE via the modular `taskTypes` slot
|
|
15
|
+
* (`registerConsumerTaskTypes`, fed once at boot from the resolved manifest);
|
|
16
|
+
* - the deployment's BACKEND-registered task types, folded into the single per-workspace
|
|
17
|
+
* {@link RemoteModuleManifest} the workspace store swaps per snapshot (`hydrateCapabilities`,
|
|
18
|
+
* reading its OWN `taskTypes` slot off the shared manifest).
|
|
19
|
+
*
|
|
20
|
+
* The merged custom catalog is projected back into `catalog.ts`'s {@link setCustomTaskTypeMeta}
|
|
21
|
+
* read-model so the pure `taskTypeMeta` lookup (used by the card badge + create-task picker)
|
|
22
|
+
* resolves a custom type reactively without importing this store.
|
|
23
|
+
*/
|
|
24
|
+
export const useTaskTypesStore = defineStore('taskTypes', () => {
|
|
25
|
+
// CODE-shipped consumer task types from the static `taskTypes` slot (fed once at boot by the
|
|
26
|
+
// modular install plugin — module slots are resolved once).
|
|
27
|
+
const consumerTaskTypes = ref<CustomTaskType[]>([])
|
|
28
|
+
// The active per-workspace capability manifest (shared with the agents store), or null before
|
|
29
|
+
// the first hydrate. This store reads only its own `taskTypes` slot off it.
|
|
30
|
+
const capabilitiesManifest = ref<RemoteModuleManifest<AppSlots> | null>(null)
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The merged CUSTOM task types (consumer-slot → backend-manifest), de-duplicated and never
|
|
34
|
+
* shadowing a BUILT-IN type. A namespaced custom id can't collide with a bare built-in, but the
|
|
35
|
+
* built-in drop is kept for parity with the agents store (and to be robust to a malformed id).
|
|
36
|
+
*/
|
|
37
|
+
const customTaskTypes = computed<CustomTaskType[]>(() => {
|
|
38
|
+
const seen = new Set<string>()
|
|
39
|
+
const out: CustomTaskType[] = []
|
|
40
|
+
const add = (t: CustomTaskType) => {
|
|
41
|
+
if (t.taskType in TASK_TYPE_META || seen.has(t.taskType)) return
|
|
42
|
+
seen.add(t.taskType)
|
|
43
|
+
out.push(t)
|
|
44
|
+
}
|
|
45
|
+
for (const t of consumerTaskTypes.value) add(t)
|
|
46
|
+
for (const t of capabilitiesManifest.value?.slots?.taskTypes ?? []) add(t)
|
|
47
|
+
return out
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
/** The custom types indexed by id, for a per-type lookup (e.g. the create-form field descriptors). */
|
|
51
|
+
const byTaskType = computed<Record<string, CustomTaskType>>(() =>
|
|
52
|
+
Object.fromEntries(customTaskTypes.value.map((t) => [t.taskType, t])),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
// Keep `catalog.ts`'s pure-util projection in sync with the merged custom catalog so
|
|
56
|
+
// `taskTypeMeta` resolves custom types. Sync flush so an imperative read right after
|
|
57
|
+
// `hydrateCapabilities` sees the fresh catalog with no tick gap (mirrors the agents store).
|
|
58
|
+
const customMetaMap = computed<Record<string, TaskTypeMeta>>(() =>
|
|
59
|
+
Object.fromEntries(customTaskTypes.value.map((t) => [t.taskType, customTaskTypeToMeta(t)])),
|
|
60
|
+
)
|
|
61
|
+
watch(customMetaMap, (map) => setCustomTaskTypeMeta(map), { immediate: true, flush: 'sync' })
|
|
62
|
+
|
|
63
|
+
/** The registered custom task type for `taskType`, or undefined. */
|
|
64
|
+
function get(taskType: string): CustomTaskType | undefined {
|
|
65
|
+
return byTaskType.value[taskType]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Register the deployment's CODE-shipped consumer task types — the resolved modular `taskTypes`
|
|
70
|
+
* slot, fed once by the install plugin. Idempotent replace (module slots resolve once).
|
|
71
|
+
*/
|
|
72
|
+
function registerConsumerTaskTypes(types: readonly CustomTaskType[]) {
|
|
73
|
+
consumerTaskTypes.value = [...types]
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Hydrate the deployment's BACKEND-registered capabilities from the shared per-workspace manifest
|
|
78
|
+
* (built by the workspace store from the snapshot, carrying both `agentKinds` + `taskTypes`).
|
|
79
|
+
* Skips the swap — and the downstream projection invalidation — when the content-derived manifest
|
|
80
|
+
* version is unchanged, exactly like the agents store.
|
|
81
|
+
*/
|
|
82
|
+
function hydrateCapabilities(manifest: RemoteModuleManifest<AppSlots>) {
|
|
83
|
+
if (capabilitiesManifest.value?.version === manifest.version) return
|
|
84
|
+
capabilitiesManifest.value = manifest
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return { customTaskTypes, get, registerConsumerTaskTypes, hydrateCapabilities }
|
|
88
|
+
})
|
package/app/stores/workspace.ts
CHANGED
|
@@ -25,6 +25,8 @@ import { useRecurringPipelinesStore } from '~/stores/recurringPipelines'
|
|
|
25
25
|
import { useInitiativesStore } from '~/stores/initiative'
|
|
26
26
|
import { useServicesStore } from '~/stores/services'
|
|
27
27
|
import { useAgentsStore } from '~/stores/agents'
|
|
28
|
+
import { useTaskTypesStore } from '~/stores/taskTypes'
|
|
29
|
+
import { buildWorkspaceCapabilitiesManifest } from '~/modular/capabilities'
|
|
28
30
|
import { useSkillsStore } from '~/stores/skills'
|
|
29
31
|
import { useTrackerStore } from '~/stores/tracker'
|
|
30
32
|
import { useRequirementsStore } from '~/stores/requirements'
|
|
@@ -169,10 +171,18 @@ export const useWorkspaceStore = defineStore(
|
|
|
169
171
|
useInitiativesStore().hydratePresets(snapshot.initiativePresets)
|
|
170
172
|
useTrackerStore().hydrate(snapshot.trackerSettings)
|
|
171
173
|
useServicesStore().hydrate(snapshot.mounts ?? [], snapshot.serviceCatalog ?? [])
|
|
172
|
-
// Hydrate the deployment's backend-registered
|
|
173
|
-
// remote capability manifest
|
|
174
|
-
//
|
|
175
|
-
|
|
174
|
+
// Hydrate the deployment's backend-registered capabilities from ONE shared per-workspace
|
|
175
|
+
// remote capability manifest carrying BOTH custom agent kinds AND custom task types (its
|
|
176
|
+
// version covers both, so an unchanged snapshot no-ops both stores). Each store reads its
|
|
177
|
+
// own slot off it, so a proprietary agent kind renders as a first-class palette block +
|
|
178
|
+
// result view and a proprietary task type as a first-class create-task choice + card badge.
|
|
179
|
+
// Swapped wholesale per workspace (no global-catalog mutation).
|
|
180
|
+
const capabilities = buildWorkspaceCapabilitiesManifest(
|
|
181
|
+
snapshot.customAgentKinds ?? [],
|
|
182
|
+
snapshot.customTaskTypes ?? [],
|
|
183
|
+
)
|
|
184
|
+
useAgentsStore().hydrateCapabilities(capabilities)
|
|
185
|
+
useTaskTypesStore().hydrateCapabilities(capabilities)
|
|
176
186
|
// The account's repo-sourced Claude Skills catalog (shared across its workspaces), so the
|
|
177
187
|
// pipeline builder's per-step skill picker has its options. A straight replace.
|
|
178
188
|
useSkillsStore().hydrate(snapshot.skills ?? [])
|
package/app/types/domain.ts
CHANGED
|
@@ -59,6 +59,10 @@ export type {
|
|
|
59
59
|
AgentKind,
|
|
60
60
|
AgentCategory,
|
|
61
61
|
CustomAgentKind,
|
|
62
|
+
CustomTaskType,
|
|
63
|
+
TaskTypePresentation,
|
|
64
|
+
TaskTypeFieldDescriptor,
|
|
65
|
+
TaskTypeFieldOption,
|
|
62
66
|
Pipeline,
|
|
63
67
|
PipelinePurpose,
|
|
64
68
|
SpendStatus,
|
|
@@ -117,6 +121,25 @@ export interface AgentArchetype {
|
|
|
117
121
|
resultView?: string
|
|
118
122
|
}
|
|
119
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Display metadata for a task TYPE (the card badge + create-task picker), resolved through the
|
|
126
|
+
* `taskTypeMeta` read-model. A BUILT-IN type carries an i18n {@link labelKey}; a CUSTOM
|
|
127
|
+
* (deployment-registered) type carries a literal {@link label} from the wire presentation. The
|
|
128
|
+
* renderer resolves the display string as `labelKey ? t(labelKey) : label`. Frontend-only.
|
|
129
|
+
*/
|
|
130
|
+
export interface TaskTypeMeta {
|
|
131
|
+
/** The task type id this meta describes. */
|
|
132
|
+
taskType: string
|
|
133
|
+
/** iconify name (lucide). */
|
|
134
|
+
icon: string
|
|
135
|
+
/** tailwind-ish accent token used across the card badge / picker. */
|
|
136
|
+
color: string
|
|
137
|
+
/** i18n key for a BUILT-IN type's label; absent for a custom type. */
|
|
138
|
+
labelKey?: string
|
|
139
|
+
/** Literal label for a CUSTOM type (from the wire presentation); absent for a built-in. */
|
|
140
|
+
label?: string
|
|
141
|
+
}
|
|
142
|
+
|
|
120
143
|
/** Level-of-detail buckets driven by the canvas zoom level. Shallow → deep:
|
|
121
144
|
* `far`/`mid`/`close` govern a service frame (chip → card → opened with tasks);
|
|
122
145
|
* `steps`/`subtasks` drill spatially into an individual task — revealing its
|
package/app/types/execution.ts
CHANGED
package/app/types/fragments.ts
CHANGED
package/app/utils/catalog.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
AgentKind,
|
|
6
6
|
BlockStatus,
|
|
7
7
|
BlockType,
|
|
8
|
+
TaskTypeMeta,
|
|
8
9
|
} from '~/types/domain'
|
|
9
10
|
|
|
10
11
|
/** Simple unique id helper (fine for a client-only prototype). */
|
|
@@ -698,6 +699,112 @@ export function isKnownAgentKind(kind: string): boolean {
|
|
|
698
699
|
return kind in AGENT_BY_KIND || kind in SYSTEM_AGENT_META || kind in customAgentKindMeta.value
|
|
699
700
|
}
|
|
700
701
|
|
|
702
|
+
// ---------------------------------------------------------------------------
|
|
703
|
+
// Task-type presentation (frontend-extension-mechanism initiative, slice B) — the exact twin of
|
|
704
|
+
// the agent-kind read-model above: a frozen BUILT-IN map + a reactive CUSTOM projection fed by the
|
|
705
|
+
// task-types store, resolved through {@link taskTypeMeta} so the card badge + create-task picker
|
|
706
|
+
// render a built-in OR a deployment-registered custom type — and an UNREGISTERED namespaced type
|
|
707
|
+
// (stale data after an extension was removed) degrades to the `feature` presentation, never a crash.
|
|
708
|
+
// ---------------------------------------------------------------------------
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* The BUILT-IN task-type presentations, keyed by type. Icons mirror the create-task picker's old
|
|
712
|
+
* hardcoded list; `labelKey` is the i18n key the renderer resolves (built-in labels are localized,
|
|
713
|
+
* unlike a custom type's literal wire label). `recurring` is not human-creatable but is stamped on
|
|
714
|
+
* the reused schedule block, so it needs a badge too.
|
|
715
|
+
*/
|
|
716
|
+
export const TASK_TYPE_META: Record<string, TaskTypeMeta> = {
|
|
717
|
+
feature: {
|
|
718
|
+
taskType: 'feature',
|
|
719
|
+
icon: 'i-lucide-sparkles',
|
|
720
|
+
color: '#38bdf8',
|
|
721
|
+
labelKey: 'board.addTask.types.feature',
|
|
722
|
+
},
|
|
723
|
+
bug: {
|
|
724
|
+
taskType: 'bug',
|
|
725
|
+
icon: 'i-lucide-bug',
|
|
726
|
+
color: '#f87171',
|
|
727
|
+
labelKey: 'board.addTask.types.bug',
|
|
728
|
+
},
|
|
729
|
+
document: {
|
|
730
|
+
taskType: 'document',
|
|
731
|
+
icon: 'i-lucide-file-text',
|
|
732
|
+
color: '#c084fc',
|
|
733
|
+
labelKey: 'board.addTask.types.document',
|
|
734
|
+
},
|
|
735
|
+
spike: {
|
|
736
|
+
taskType: 'spike',
|
|
737
|
+
icon: 'i-lucide-flask-conical',
|
|
738
|
+
color: '#fbbf24',
|
|
739
|
+
labelKey: 'board.addTask.types.spike',
|
|
740
|
+
},
|
|
741
|
+
review: {
|
|
742
|
+
taskType: 'review',
|
|
743
|
+
icon: 'i-lucide-clipboard-check',
|
|
744
|
+
color: '#34d399',
|
|
745
|
+
labelKey: 'board.addTask.types.review',
|
|
746
|
+
},
|
|
747
|
+
ralph: {
|
|
748
|
+
taskType: 'ralph',
|
|
749
|
+
icon: 'i-lucide-infinity',
|
|
750
|
+
color: '#a78bfa',
|
|
751
|
+
labelKey: 'board.addTask.types.ralph',
|
|
752
|
+
},
|
|
753
|
+
recurring: {
|
|
754
|
+
taskType: 'recurring',
|
|
755
|
+
icon: 'i-lucide-repeat',
|
|
756
|
+
color: '#94a3b8',
|
|
757
|
+
labelKey: 'board.addTask.types.recurring',
|
|
758
|
+
},
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* Reactive read-model of the deployment's CUSTOM task types (consumer-slot + backend
|
|
763
|
+
* remote-manifest), kept in sync by the task-types store — the exact twin of
|
|
764
|
+
* {@link customAgentKindMeta}. The store is the source of truth; this `shallowRef` is its
|
|
765
|
+
* synchronous projection so {@link taskTypeMeta} / {@link isKnownTaskType} resolve a custom type
|
|
766
|
+
* WITHOUT importing the store (circular) or mutating {@link TASK_TYPE_META}.
|
|
767
|
+
*/
|
|
768
|
+
const customTaskTypeMeta = shallowRef<Record<string, TaskTypeMeta>>({})
|
|
769
|
+
|
|
770
|
+
/** Replace the custom task-type projection (called only by the task-types store). */
|
|
771
|
+
export function setCustomTaskTypeMeta(map: Record<string, TaskTypeMeta>): void {
|
|
772
|
+
customTaskTypeMeta.value = map
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/** Test-only: clear the custom task-type projection so a spec starts from built-ins only. */
|
|
776
|
+
export function __resetCustomTaskTypeMetaForTest(): void {
|
|
777
|
+
customTaskTypeMeta.value = {}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* Resolve display metadata for ANY task type — a BUILT-IN ({@link TASK_TYPE_META}), a deployment
|
|
782
|
+
* CUSTOM type (projected into {@link customTaskTypeMeta} by the task-types store), or an
|
|
783
|
+
* UNREGISTERED namespaced one (stale data after its extension was removed) — ALWAYS returning a
|
|
784
|
+
* usable icon/color/label. An unregistered/unknown type falls back to the `feature` presentation
|
|
785
|
+
* (its icon/color) but keeps the raw id as its literal label, so a leftover string renders a badge
|
|
786
|
+
* instead of breaking the card. Reading `customTaskTypeMeta` reactively means a component computed
|
|
787
|
+
* re-runs when the custom catalog changes.
|
|
788
|
+
*/
|
|
789
|
+
export function taskTypeMeta(taskType: string | undefined): TaskTypeMeta {
|
|
790
|
+
if (!taskType) return TASK_TYPE_META.feature!
|
|
791
|
+
return (
|
|
792
|
+
TASK_TYPE_META[taskType] ??
|
|
793
|
+
customTaskTypeMeta.value[taskType] ?? {
|
|
794
|
+
taskType,
|
|
795
|
+
icon: TASK_TYPE_META.feature!.icon,
|
|
796
|
+
color: TASK_TYPE_META.feature!.color,
|
|
797
|
+
// Unregistered namespaced type: no i18n key, so show its raw id rather than a blank badge.
|
|
798
|
+
label: taskType,
|
|
799
|
+
}
|
|
800
|
+
)
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/** Whether a task type is known to this build — a built-in or a projected custom type. */
|
|
804
|
+
export function isKnownTaskType(taskType: string): boolean {
|
|
805
|
+
return taskType in TASK_TYPE_META || taskType in customTaskTypeMeta.value
|
|
806
|
+
}
|
|
807
|
+
|
|
701
808
|
type BlockTypeMeta = { label: string; icon: string; accent: string }
|
|
702
809
|
|
|
703
810
|
/** Visual metadata for each architecture block type. */
|
package/i18n/locales/de.json
CHANGED
|
@@ -1476,7 +1476,20 @@
|
|
|
1476
1476
|
"confirmReject": "Ablehnung bestätigen",
|
|
1477
1477
|
"requestChanges": "Änderungen anfordern",
|
|
1478
1478
|
"reject": "Ablehnen",
|
|
1479
|
-
"requestChangesHint": "Änderungen anfordern führt diesen Schritt mit deinem Feedback & deinen Kommentaren erneut aus. Ablehnen stoppt den Lauf vollständig."
|
|
1479
|
+
"requestChangesHint": "Änderungen anfordern führt diesen Schritt mit deinem Feedback & deinen Kommentaren erneut aus. Ablehnen stoppt den Lauf vollständig.",
|
|
1480
|
+
"effort": {
|
|
1481
|
+
"heading": "Agenten-Aufwand",
|
|
1482
|
+
"difficulty": "Schwierigkeit",
|
|
1483
|
+
"outOfTen": "{value}/10",
|
|
1484
|
+
"reduced": "Was die Effektivität verringert hat",
|
|
1485
|
+
"obstacles": "Wichtigste Hindernisse"
|
|
1486
|
+
},
|
|
1487
|
+
"adherence": {
|
|
1488
|
+
"heading": "Einhaltung der Best Practices",
|
|
1489
|
+
"outOfTen": "{value}/10",
|
|
1490
|
+
"relatedFindings": "Zugehörige Befunde",
|
|
1491
|
+
"unnamed": "Standard"
|
|
1492
|
+
}
|
|
1480
1493
|
},
|
|
1481
1494
|
"inspector": {
|
|
1482
1495
|
"frameStatus": {
|
|
@@ -3565,7 +3578,10 @@
|
|
|
3565
3578
|
"summaryPlaceholder": "Einzeilige Zusammenfassung (vom Selektor verwendet)",
|
|
3566
3579
|
"bodyPlaceholder": "Richtlinientext (in den Prompt injiziert)",
|
|
3567
3580
|
"tagsPlaceholder": "Tags, kommagetrennt (z. B. backend, db)",
|
|
3568
|
-
"add": "Fragment hinzufügen"
|
|
3581
|
+
"add": "Fragment hinzufügen",
|
|
3582
|
+
"generateTitle": "Generieren",
|
|
3583
|
+
"generateTitleHint": "Titel aus dem Inhalt des Fragments vorschlagen",
|
|
3584
|
+
"titleGenFailed": "Titel konnte nicht generiert werden"
|
|
3569
3585
|
},
|
|
3570
3586
|
"documents": {
|
|
3571
3587
|
"intro": "Verknüpfen Sie eine Confluence-/Notion-Seite oder eine GitHub-Datei als Best-Practice-Fragment. Ihre Richtlinie wird zur Laufzeit aus der Quelle neu aufgelöst: Bearbeiten Sie das Dokument, und der nächste Agentenlauf folgt der neuen Version (kein erneuter Import).",
|
|
@@ -3615,7 +3631,9 @@
|
|
|
3615
3631
|
"checkSourceFailed": "Quelle konnte nicht geprüft werden",
|
|
3616
3632
|
"sourceUnlinked": "Quelle-Verknüpfung aufgehoben",
|
|
3617
3633
|
"unlinkSourceFailed": "Quelle-Verknüpfung konnte nicht aufgehoben werden",
|
|
3618
|
-
"documentsLinked": "{count} Dokument als lebendes Fragment verknüpft | {count} Dokumente als lebende Fragmente verknüpft"
|
|
3634
|
+
"documentsLinked": "{count} Dokument als lebendes Fragment verknüpft | {count} Dokumente als lebende Fragmente verknüpft",
|
|
3635
|
+
"updated": "Fragment aktualisiert",
|
|
3636
|
+
"updateFailed": "Fragment konnte nicht aktualisiert werden"
|
|
3619
3637
|
},
|
|
3620
3638
|
"confirmRemove": {
|
|
3621
3639
|
"title": "Dieses Fragment löschen?",
|
|
@@ -3984,7 +4002,8 @@
|
|
|
3984
4002
|
"body": "Du hast Änderungen vorgenommen, die noch nicht gespeichert wurden. Schließen und verlieren?",
|
|
3985
4003
|
"confirm": "Verwerfen",
|
|
3986
4004
|
"keep": "Weiter bearbeiten"
|
|
3987
|
-
}
|
|
4005
|
+
},
|
|
4006
|
+
"edit": "Bearbeiten"
|
|
3988
4007
|
},
|
|
3989
4008
|
"access": {
|
|
3990
4009
|
"noBoardWrite": "Nur-Lese-Zugriff: Sie können dieses Board ansehen, aber nicht bearbeiten.",
|